go / jitter

I use a ticker with jitter to avoid thundering herd problems when many clients poll or retry at the same interval.

The problem

When multiple clients use the same interval for polling or retries, they can synchronize and hit a server simultaneously:

// 100 clients all polling every 30 seconds
// will spike load every 30 seconds
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
    poll()
}

The pattern

Add random variation to each interval:

type Jitter struct {
	Base      time.Duration
	Deviation time.Duration
}

func (j Jitter) Duration() time.Duration {
	base := j.Base - j.Deviation
	jitter := time.Duration(rand.Int64N(int64(2 * j.Deviation)))
	return base + jitter
}

A Jitter{Base: 30*time.Second, Deviation: 5*time.Second} produces durations uniformly distributed between 25 and 35 seconds.

Ticker with jitter

Wrap time.Timer to create a ticker that varies each interval:

type TickerWithJitter struct {
	C      chan time.Time
	jitter Jitter
	stop   func()
}

func NewTickerWithJitter(base, deviation time.Duration) *TickerWithJitter {
	ctx, cancel := context.WithCancel(context.Background())
	t := &TickerWithJitter{
		C:      make(chan time.Time),
		jitter: Jitter{Base: base, Deviation: deviation},
		stop:   cancel,
	}
	go t.run(ctx)
	return t
}

func (t *TickerWithJitter) run(ctx context.Context) {
	timer := time.NewTimer(t.jitter.Duration())
	defer timer.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case now := <-timer.C:
			timer.Reset(t.jitter.Duration())
			t.C <- now
		}
	}
}

func (t *TickerWithJitter) Stop() {
	t.stop()
}

Usage

// Poll every 30s ± 5s
ticker := NewTickerWithJitter(30*time.Second, 5*time.Second)
defer ticker.Stop()

for range ticker.C {
    poll()
}

When to use

When not to use

See backoff for retry delay strategies and sleepctx for context-aware sleeping.

← All articles