ubgo is in early alpha: libraries and APIs may still change.

Skip to content
ubgo Alpha

Cache stampede protection with Remember

When many goroutines miss the same key at once, Remember runs your loader one time and hands the result to all of them.

Based on the cache README, section “How `Remember` decides” About cache →

Step 1 of 6

30 callers ask for one key

The cache is empty. 30 goroutines call Remember for user:42 at the same moment, each with the same loader function.

All steps as text
  1. 30 callers ask for one key. The cache is empty. 30 goroutines call Remember for user:42 at the same moment, each with the same loader function.
  2. Every caller misses. Get finds nothing, so each caller sees a miss (ErrNotFound). Without protection, all 30 would now run the loader and hit the database.
  3. Only one runs the loader. The miss goes into a single-flight loader. One goroutine runs your function; the other 29 block and wait for its answer.
  4. The value is stored. The loader returns. Remember wraps the value in a small envelope and stores it with Set, using the hard TTL plus jitter.
  5. All 30 share one result. The 29 waiting goroutines get the same value. The loader ran exactly once for 30 callers.
  6. Later calls are cache hits. A caller that arrives while the entry is still fresh gets the cached value straight away. The loader is not called.

The README describes it in one line: 30 goroutines that all miss user:42 at once run the loader exactly once; the other 29 block and share the result.

p, err := cache.Remember(ctx, c, "product:99", 10*time.Minute,
    func(ctx context.Context) (Product, error) { return db.Product(ctx, 99) },
)

Add WithJitter so a batch of keys written together do not all expire in the same instant, and WithRefreshAhead so a hot key is refreshed in the background before it expires. The freshness explainer shows those windows.