- Benchmark Results
- Installation
- Quick Start
- Configuration
- Architecture
- API Reference
- HTTP Middleware
- Cache Invalidation Setup
# Run comparison benchmarks
make bench-compare
# Run stress tests
make stress-test
# Run all benchmarks with the benchmark runner
make bench-runner
# Run all benchmark tests
make bench
Test Environment:
- OS: macOS (Darwin)
- Architecture: ARM64
- CPU: Apple M4 Max
- Go Version: 1.21+
Cache Library | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
Kioshun | 100,000,000 | 53.95 | 41 | 3 |
Ristretto | 83,301,469 | 82.08 | 153 | 5 |
BigCache | 38,670,102 | 154.1 | 40 | 2 |
go-cache | 20,022,591 | 353.3 | 57 | 3 |
freecache | 13,592,491 | 523.3 | 903 | 2 |
Cache Library | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
Ristretto | 287,087,389 | 21.22 | 31 | 2 |
Kioshun | 281,013,220 | 21.78 | 31 | 2 |
BigCache | 100,000,000 | 79.34 | 1047 | 3 |
freecache | 80,708,305 | 77.29 | 1039 | 2 |
go-cache | 45,681,829 | 136.2 | 15 | 1 |
Cache Library | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
Kioshun | 125,047,722 | 48.47 | 31 | 2 |
Ristretto | 85,749,963 | 60.89 | 69 | 3 |
BigCache | 36,268,677 | 148.9 | 743 | 3 |
go-cache | 30,280,268 | 201.9 | 22 | 2 |
freecache | 21,571,833 | 273.3 | 1039 | 2 |
Cache Library | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
Kioshun | 97,568,907 | 69.87 | 34 | 2 |
BigCache | 36,030,003 | 167.2 | 558 | 2 |
go-cache | 28,345,117 | 235.5 | 34 | 1 |
Ristretto | 27,784,916 | 222.6 | 83 | 3 |
freecache | 18,082,710 | 329.7 | 919 | 2 |
Cache Library | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
Ristretto | 106,750,716 | 33.20 | 45 | 3 |
Kioshun | 96,229,144 | 37.01 | 31 | 2 |
BigCache | 26,457,594 | 130.0 | 946 | 3 |
freecache | 23,158,801 | 157.0 | 1039 | 2 |
go-cache | 19,328,863 | 183.2 | 18 | 2 |
Cache Library | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
Kioshun | 100,000,000 | 33.74 | 31 | 2 |
Ristretto | 25,788,711 | 163.6 | 132 | 5 |
BigCache | 21,301,932 | 168.7 | 133 | 3 |
go-cache | 12,469,190 | 279.9 | 37 | 2 |
freecache | 9,007,708 | 380.0 | 1039 | 2 |
Cache Library | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
Kioshun | 58,979,691 | 60.30 | 48 | 3 |
Ristretto | 26,206,014 | 120.9 | 97 | 3 |
BigCache | 20,647,864 | 183.7 | 816 | 3 |
go-cache | 16,281,172 | 229.4 | 40 | 2 |
freecache | 9,182,894 | 383.5 | 1167 | 2 |
Cache Library | Ops/sec | ns/op | bytes/op |
---|---|---|---|
Kioshun | 94,229,144 | 38.01 | 31.0 |
Ristretto | 106,750,716 | 33.20 | 45.0 |
BigCache | 26,457,594 | 130.0 | 946.0 |
freecache | 23,158,801 | 157.0 | 1039.0 |
go-cache | 19,328,863 | 183.2 | 18.0 |
- 21-82ns per operation for most cache operations
- 58+ million operations/sec throughput
- Peak performance: 275M+ operations/sec for GET operations
Policy | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
FIFO | 47,275,994 | 152.4 | 59 | 3 |
LRU | 38,241,679 | 157.9 | 59 | 3 |
Random | 23,302,548 | 170.3 | 119 | 3 |
LFU | 39,562,033 | 191.9 | 60 | 3 |
Load Profile | Ops/sec | ns/op | B/op | allocs/op | Description |
---|---|---|---|---|---|
Small + High Concurrency | 59,745,292 | 59.50 | 27 | 2 | Many goroutines, small cache |
Medium + Mixed Load | 56,104,041 | 64.83 | 31 | 2 | Balanced read/write operations |
Large + Read Heavy | 56,693,991 | 65.72 | 38 | 2 | Large cache, mostly reads |
XLarge + Write Heavy | 44,826,436 | 68.11 | 40 | 3 | Very large cache, mostly writes |
Extreme + Balanced | 45,561,304 | 80.95 | 41 | 3 | Maximum scale, balanced ops |
Test | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
High Contention | 49,839,693 | 68.86 | 34 | 2 |
Value Size | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
64KB | 56,616,940 | 66.20 | 40 | 3 |
1KB | 55,313,698 | 66.37 | 40 | 3 |
4KB | 56,154,799 | 65.81 | 40 | 2 |
16KB | 55,306,689 | 68.36 | 40 | 3 |
Shards | Ops/sec | ns/op | B/op | allocs/op |
---|---|---|---|---|
1 | 9,378,514 | 426.8 | 43 | 3 |
2 | 11,517,030 | 340.5 | 43 | 3 |
4 | 16,063,339 | 232.1 | 43 | 3 |
8 | 23,789,816 | 154.2 | 45 | 3 |
16 | 32,913,006 | 105.1 | 46 | 3 |
32 | 42,531,112 | 80.99 | 47 | 3 |
64 | 52,462,201 | 67.55 | 49 | 3 |
128 | 59,367,160 | 56.80 | 49 | 3 |
Kioshun uses a sharded architecture to minimize lock contention:
┌─────────────────────────────────────────────────────────────┐
│ Kioshun Cache │
├─────────────┬─────────────┬─────────────┬───────────────────┤
│ Shard 0 │ Shard 1 │ Shard 2 │ ... │ Shard N │
│ RWMutex │ RWMutex │ RWMutex │ │ RWMutex │
│ LRU List │ LRU List │ LRU List │ │ LRU List│
│ LFU Heap │ LFU Heap │ LFU Heap │ │ LFU Heap│
│ Hash Map │ Hash Map │ Hash Map │ │ Hash Map│
│ Stats │ Stats │ Stats │ │ Stats │
└─────────────┴─────────────┴─────────────┴───────────────────┘
Key Design Principles:
- Automatic Sharding: Keys distributed across shards using optimized hash functions
- Minimal Lock Contention: Each shard maintains independent read-write mutex
- Optimal Shard Count: Auto-detection based on CPU cores
- Memory Efficiency: Object pooling to reduce GC pressure
- Integer Keys: xxHash64 avalanche mixing for optimal distribution
- String Keys: FNV-1a for short strings (≤8 bytes), xxHash64 for longer strings (>8 bytes)
- Other Types: Fallback to string conversion with hybrid string hashing
- Access: Move to head in O(1)
- Eviction: Remove tail in O(1)
- Memory: Minimal overhead per item
Uses min-heap for efficient frequency-based eviction:
- Access: Update frequency and rebalance heap in O(log n)
- Eviction: Remove minimum frequency item in O(log n)
- Memory: Additional heap index per item
LFU Heap Structure:
Min Frequency Item (Root)
/ \
Higher Freq Higher Freq
/ \ / \
Items Items Items Items
- Access: No updates required on access
- Eviction: Remove oldest item (at tail) in O(1)
- Memory: Minimal overhead per item
- Access: No updates required on access
- Eviction: Randomly select item using time-based pseudo-randomness in O(n)
- Memory: No additional overhead per item
- Algorithm: Uses
time.Now().UnixNano() % len(keys)
for selection
Eviction Algorithm Comparison:
Policy | Access Time | Eviction Time | Memory Overhead | Best Use Case |
---|---|---|---|---|
LRU | O(1) | O(1) | Low | General purpose |
LFU | O(log n) | O(log n) | Medium | Frequency-based access |
FIFO | O(1) | O(1) | Low | Simple time-based |
Random | O(1) | O(n) | Low | Cache-oblivious workloads |
The sharded design enables high-throughput concurrent access:
- Read Operations: Multiple goroutines can read from different shards simultaneously
- Write Operations: Writers only block other operations on the same shard
- Cross-Shard Operations: Statistics aggregation uses atomic operations to avoid blocking
- Object Pooling: Reuses
cacheItem
objects to reduce GC pressure - Atomic Statistics: Per-shard metrics tracked with atomic operations
- Lazy Initialization: LFU heaps only created when LFU policy is selected
- Efficient Cleanup: Background goroutine removes expired items periodically
const (
LRU EvictionPolicy = iota // Least Recently Used (default)
LFU // Least Frequently Used
FIFO // First In, First Out
Random // Random eviction
)
go get github.com/unkn0wn-root/kioshun
package main
import (
"fmt"
"time"
cache "github.com/unkn0wn-root/kioshun"
)
func main() {
// Create cache with default configuration
c := cache.NewWithDefaults[string, string]()
defer c.Close()
// Set with default TTL (30 min)
c.Set("user:123", "David Nice", cache.DefaultExpiration)
// Set with no expiration
c.Set("user:123", "David Nice", cache.NoExpiration)
// Set value with custom TTL
c.Set("user:123", "David Nice", 5*time.Minute)
// Get value
if value, found := c.Get("user:123"); found {
fmt.Printf("User: %s\n", value)
}
// Get cache statistics
stats := c.Stats()
fmt.Printf("Hit ratio: %.2f%%\n", stats.HitRatio*100)
}
config := cache.Config{
MaxSize: 100000, // Maximum number of items
ShardCount: 16, // Number of shards (0 = auto-detect)
CleanupInterval: 5 * time.Minute, // Cleanup frequency
DefaultTTL: 30 * time.Minute, // Default expiration time
EvictionPolicy: cache.LRU, // Eviction algorithm
StatsEnabled: true, // Enable statistics collection
}
cache := cache.New[string, any](config)
Kioshun provides several preset configurations for common use cases. These presets combine appropriate settings for cache size, shard count, cleanup intervals, TTL, and eviction policies based on typical usage patterns.
// For session storage
sessionCache := cache.New[string, Session](cache.SessionCacheConfig())
// For API response caching
apiCache := cache.New[string, APIResponse](cache.APICacheConfig())
// For temporary data
tempCache := cache.New[string, any](cache.TemporaryCacheConfig())
// For persistent data
persistentCache := cache.New[string, any](cache.PersistentCacheConfig())
// For user data caching
userCache := cache.New[string, User](cache.UserCacheConfig())
// For high-performance scenarios
hpCache := cache.New[string, any](cache.HPCacheConfig())
// For memory-constrained env.
lowMemCache := cache.New[string, any](cache.LowMemoryCacheConfig())
// Set stores a value with TTL
cache.Set(key, value, ttl time.Duration) error
// Get retrieves a value
cache.Get(key) (value, found bool)
// GetWithTTL retrieves a value with remaining TTL
cache.GetWithTTL(key) (value, ttl time.Duration, found bool)
// Delete removes a key
cache.Delete(key) bool
// Exists checks if a key exists without updating access time
cache.Exists(key) bool
// Clear removes all items
cache.Clear()
// Size returns current item count
cache.Size() int64
// Stats returns performance statistics
cache.Stats() Stats
// Close gracefully shuts down the cache
cache.Close() error
// Set with expiration callback
cache.SetWithCallback(key, value, ttl, callback func(key, value)) error
// Get all keys (expensive operation)
cache.Keys() []K
// Trigger manual cleanup
cache.TriggerCleanup()
type Stats struct {
Hits int64 // Cache hits
Misses int64 // Cache misses
Evictions int64 // LRU evictions
Expirations int64 // TTL expirations
Size int64 // Current items
Capacity int64 // Maximum items
HitRatio float64 // Hit ratio (0.0-1.0)
Shards int // Number of shards
}
config := cache.DefaultMiddlewareConfig()
config.DefaultTTL = 5 * time.Minute
config.MaxSize = 100000
config.ShardCount = 16
middleware := cache.NewHTTPCacheMiddleware(config)
defer middleware.Close()
// IMPORTANT: Enable invalidation if needed
// middleware.SetKeyGenerator(cache.KeyWithoutQuery())
// Use with any HTTP framework
http.Handle("/api/users", middleware.Middleware(usersHandler))
// Standard net/http
http.Handle("/api/users", middleware.Middleware(handler))
// Gin Framework
router.Use(gin.WrapH(middleware.Middleware(http.DefaultServeMux)))
// Echo Framework
e.Use(echo.WrapMiddleware(middleware.Middleware))
// Chi Router
r.Use(middleware.Middleware)
// Gorilla Mux
r.Use(middleware.Middleware)
// High-performance API caching
apiConfig := cache.DefaultMiddlewareConfig()
apiConfig.MaxSize = 50000
apiConfig.ShardCount = 32
apiConfig.DefaultTTL = 10 * time.Minute
apiConfig.MaxBodySize = 5 * 1024 * 1024 // 5MB
apiMiddleware := cache.NewHTTPCacheMiddleware(apiConfig)
// Enable invalidation for API endpoints
apiMiddleware.SetKeyGenerator(cache.KeyWithoutQuery())
// User-specific caching
userMiddleware := cache.NewHTTPCacheMiddleware(config)
userMiddleware.SetKeyGenerator(cache.KeyWithUserID("X-User-ID"))
// Note: User-specific caching uses different key format - invalidation works differently
// Content-type based caching with different TTLs
contentMiddleware := cache.NewHTTPCacheMiddleware(config)
contentMiddleware.SetKeyGenerator(cache.KeyWithoutQuery()) // Enable invalidation
contentMiddleware.SetCachePolicy(cache.CacheByContentType(map[string]time.Duration{
"application/json": 5 * time.Minute,
"text/html": 10 * time.Minute,
"image/": 1 * time.Hour,
}, 2*time.Minute))
// Size-based conditional caching
conditionalMiddleware := cache.NewHTTPCacheMiddleware(config)
conditionalMiddleware.SetKeyGenerator(cache.KeyWithoutQuery()) // Enable invalidation
conditionalMiddleware.SetCachePolicy(cache.CacheBySize(100, 1024*1024, 3*time.Minute))
// Default key generator (method + URL + vary headers)
middleware.SetKeyGenerator(cache.DefaultKeyGenerator)
// User-specific keys
middleware.SetKeyGenerator(cache.KeyWithUserID("X-User-ID"))
// Custom vary headers
middleware.SetKeyGenerator(cache.KeyWithVaryHeaders([]string{"Accept", "Authorization"}))
// Ignore query parameters
middleware.SetKeyGenerator(cache.KeyWithoutQuery())
// Always cache successful responses
middleware.SetCachePolicy(cache.AlwaysCache(5 * time.Minute))
// Never cache
middleware.SetCachePolicy(cache.NeverCache())
// Content-type based caching
middleware.SetCachePolicy(cache.CacheByContentType(map[string]time.Duration{
"application/json": 5 * time.Minute,
"text/html": 10 * time.Minute,
}, 2*time.Minute))
// Size-based caching
middleware.SetCachePolicy(cache.CacheBySize(100, 1024*1024, 3*time.Minute))
// Cache hit/miss callbacks
middleware.OnHit(func(key string) {
fmt.Printf("Cache hit: %s\n", key)
})
middleware.OnMiss(func(key string) {
fmt.Printf("Cache miss: %s\n", key)
})
middleware.OnSet(func(key string, ttl time.Duration) {
fmt.Printf("Cache set: %s (TTL: %v)\n", key, ttl)
})
// Get cache statistics
stats := middleware.Stats()
fmt.Printf("Hit ratio: %.2f%%\n", stats.HitRatio*100)
// Get cache statistics
stats := middleware.Stats()
// Clear all cached responses
middleware.Clear()
// Invalidate by function
middleware.InvalidateByFunc(func(key string) bool {
return strings.Contains(key, "user")
})
// Close middleware
middleware.Close()
Cache invalidation by URL pattern requires specific key generator configuration.
The default key generator uses MD5 hashing which makes pattern-based invalidation impossible:
// DEFAULT SETUP - Invalidation won't work
config := cache.DefaultMiddlewareConfig()
middleware := cache.NewHTTPCacheMiddleware(config)
// This returns 0 removed entries because keys are hashed
removed := middleware.Invalidate("/api/users/*") // Returns 0
Why it fails:
- Default keys:
"a1b2c3d4e5f6..."
(MD5 hash) - Pattern matching needs:
"GET:/api/users/123"
(readable path) - Hash loses original URL information
Use path-based key generators for invalidation to work:
// CORRECT SETUP - Invalidation works
config := cache.DefaultMiddlewareConfig()
middleware := cache.NewHTTPCacheMiddleware(config)
// CRITICAL: Set path-based key generator
middleware.SetKeyGenerator(cache.KeyWithoutQuery())
// Now invalidation works
removed := middleware.Invalidate("/api/users/*") // Returns actual count
Key Generator | Example Key | Invalidation | Use Case |
---|---|---|---|
DefaultKeyGenerator |
"a1b2c3d4..." |
❌ Broken | No invalidation needed |
KeyWithoutQuery() |
"GET:/api/users" |
✅ Works | Recommended for invalidation |
PathBasedKeyGenerator |
"GET:/api/users" |
✅ Works | Simple path-based caching |
KeyWithVaryHeaders() |
"a1b2c3d4..." |
❌ Broken | Custom headers + security |
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/unkn0wn-root/kioshun"
)
func main() {
// Setup middleware
config := cache.DefaultMiddlewareConfig()
config.DefaultTTL = 10 * time.Minute
middleware := cache.NewHTTPCacheMiddleware(config)
defer middleware.Close()
// Enable invalidation
middleware.SetKeyGenerator(cache.KeyWithoutQuery())
// Setup handlers
http.Handle("/api/users", middleware.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"users": []string{"alice", "bob", "charlie"},
"cached_at": time.Now(),
})
})))
http.Handle("/api/users/", middleware.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"user": "user-data",
"cached_at": time.Now(),
})
})))
// Invalidation endpoint
http.HandleFunc("/admin/invalidate", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
pattern := r.URL.Query().Get("pattern")
if pattern == "" {
http.Error(w, "pattern parameter required", http.StatusBadRequest)
return
}
// This now works!
removed := middleware.Invalidate(pattern)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"message": fmt.Sprintf("Invalidated %d entries", removed),
"pattern": pattern,
})
})
fmt.Println("Server starting on :8080")
fmt.Println("\nTest caching:")
fmt.Println(" curl http://localhost:8080/api/users")
fmt.Println(" curl http://localhost:8080/api/users/123")
fmt.Println("\nTest invalidation:")
fmt.Println(" curl -X POST 'http://localhost:8080/admin/invalidate?pattern=/api/users/*'")
http.ListenAndServe(":8080", nil)
}
Use KeyWithoutQuery()
when:
- You need pattern-based invalidation
- Query parameters don't affect response content
- You want readable cache keys for debugging
Use DefaultKeyGenerator
when:
- You don't need pattern invalidation
- Query parameters affect response content
- You only use
Clear()
for cache management
The middleware automatically handles:
- Cache-Control headers (max-age, no-cache, no-store, private)
- Expires headers (RFC1123 format)
- ETag generation and validation
- Vary headers for content negotiation
- X-Cache headers (HIT/MISS status)
- X-Cache-Date and X-Cache-Age headers