time.NewTicker is fine for a single process, but when you have 30 replicas of a service all doing a 1-minute cache refresh, they will all fire at the same millisecond and DDoS the upstream. A little jitter spreads them out.

I also dislike remembering to defer ticker.Stop() at every call site. Wrapping it in a function that takes a context lets me skip that.

// TickWithJitter sends on the returned channel at roughly interval,
// with +/- jitter added to each tick, until ctx is done.
func TickWithJitter(ctx context.Context, interval, jitter time.Duration) <-chan time.Time {
	ch := make(chan time.Time, 1)
	go func() {
		defer close(ch)
		for {
			d := interval + time.Duration(rand.Int63n(int64(2*jitter))) - jitter
			if d < 0 {
				d = interval
			}
			t := time.NewTimer(d)
			select {
			case <-ctx.Done():
				t.Stop()
				return
			case now := <-t.C:
				select {
				case ch <- now:
				case <-ctx.Done():
					return
				}
			}
		}
	}()
	return ch
}

Usage is the same as a normal ticker. No .Stop() needed, just cancel the context. See also /posts/the-goroutine-leak-i-didnt-notice-for-six-weeks/.