Go

What problem does singleflight solve, and what are its gotchas?

Question 247HardGo 1.22 to 1.25

golang.org/x/sync/singleflight deduplicates concurrent calls for the same key. While one call for a key is in flight, later callers wait for it and share its result. This prevents a cache stampede (thundering herd): when a hot cache entry expires, thousands of requests would otherwise hit the database at the same moment.

type UserService struct {
	sf    singleflight.Group
	cache *Cache
	db    *DB
}

func (s *UserService) Get(ctx context.Context, id string) (*User, error) {
	if u, ok := s.cache.Get(id); ok {
		return u, nil
	}
	v, err, _ := s.sf.Do(id, func() (any, error) {
		// detach from the first caller's cancellation, but keep a bound
		ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second)
		defer cancel()
		u, err := s.db.LoadUser(ctx, id)
		if err == nil {
			s.cache.Set(id, u)
		}
		return u, err
	})
	if err != nil {
		return nil, err
	}
	return v.(*User), nil
}

Gotchas:

  • The function runs with the first caller's context. If that caller cancels, every waiter fails. Use WithoutCancel plus a timeout, as above.
  • Do ignores each waiter's own context. Use DoChan with a select so each caller can give up independently.
  • The result is shared, so treat it as immutable.
  • Errors are shared too. Forget(key) lets the next caller start a new call.
  • It only deduplicates within one process.

More on Concurrency Patterns & sync

All 38 Concurrency Patterns & sync questions