go
63 lines · 7 steps
How a retry budget caps retries in Go
A sliding-window rate limiter that ties allowed retries to recent traffic so retries can't overwhelm a struggling service.
Explained by
highlit
1package retry
2
3import (
4 "errors"
5 "sync"
6 "time"
7)
8
9var ErrBudgetExhausted = errors.New("retry budget exhausted")
10
11type Budget struct {
12 mu sync.Mutex
13 window time.Duration
14 max int
15 ratio float64
16 attempts []time.Time
17 retries []time.Time
18 now func() time.Time
19}
20
21func NewBudget(window time.Duration, minRetries int, ratio float64) *Budget {
22 return &Budget{
23 window: window,
24 max: minRetries,
25 ratio: ratio,
26 now: time.Now,
27 }
28}
29
30func (b *Budget) TryAcquire() error {
31 b.mu.Lock()
32 defer b.mu.Unlock()
33
34 cutoff := b.now().Add(-b.window)
35 b.attempts = prune(b.attempts, cutoff)
36 b.retries = prune(b.retries, cutoff)
37
38 limit := b.max
39 if scaled := int(float64(len(b.attempts)) * b.ratio); scaled > limit {
40 limit = scaled
41 }
42
43 if len(b.retries) >= limit {
44 return ErrBudgetExhausted
45 }
46
47 b.retries = append(b.retries, b.now())
48 return nil
49}
50
51func (b *Budget) RecordAttempt() {
52 b.mu.Lock()
53 b.attempts = append(b.attempts, b.now())
54 b.mu.Unlock()
55}
56
57func prune(ts []time.Time, cutoff time.Time) []time.Time {
58 i := 0
59 for i < len(ts) && ts[i].Before(cutoff) {
60 i++
61 }
62 return ts[i:]
63}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Scaling the retry limit to recent traffic keeps a floor while preventing retry storms under high load.
- 2A sliding window is cheap to maintain by pruning timestamps older than a cutoff on each check.
- 3Guarding all shared slices with one mutex keeps the budget's read-modify-write sequence atomic.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
rust
use std::collections::VecDeque; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant};
Building a counting semaphore in Rust
concurrency
synchronization
condition-variable
Advanced
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-retry-budget-caps-retries-in-go-explained-go-1477/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.