Migration guide¶
Moving to this library from the two most common Go resilience packages:
- From
golang.org/x/time/rate— rate limiting - From
sony/gobreaker— circuit breaking
All snippets use the real public API. The module path is
github.com/sanskarpan/Rate-Limiter-Circuit-Breaker.
From golang.org/x/time/rate¶
The key conceptual difference: keyed limiters¶
x/time/rate gives you one *rate.Limiter that governs a single stream of
events. To rate-limit per user/IP/tenant you build and manage a map[key]*rate.Limiter
yourself (plus eviction).
This library's ratelimit.Limiter is keyed: a single limiter instance holds
per-key buckets internally, and you pass the key on every call. Idle keys are
evicted automatically (see tokenbucket.WithIdleCleanup).
// x/time/rate: one limiter per stream, you manage the map
lim := rate.NewLimiter(rate.Limit(10), 20) // 10 events/s, burst 20
lim.Allow() // no key argument
// this library: one limiter, keyed per call
lim := tokenbucket.New(20, 10) // capacity 20, refill 10/s
defer lim.Close()
lim.Allow(ctx, "user:123").Allowed // key is an argument
Argument order:
rate.NewLimiter(rate.Limit(r), b)takes rate first, burst (capacity) second.tokenbucket.New(capacity, refillRate)takes capacity first, refill rate second — the reverse. Read the mapping table carefully.
API mapping¶
golang.org/x/time/rate |
This library (tokenbucket) |
Notes |
|---|---|---|
rate.NewLimiter(rate.Limit(r), b) |
tokenbucket.New(b, r) |
New(capacity, refillRate) — capacity first. capacity is the burst b; refillRate is the steady-state rate r (tokens/sec). |
lim.Allow() |
lim.Allow(ctx, key).Allowed |
Allow returns a ratelimit.Result; read .Allowed. |
lim.AllowN(time.Now(), n) |
lim.AllowN(ctx, key, n).Allowed |
Consumes n tokens all-or-nothing. |
lim.Wait(ctx) |
lim.Wait(ctx, key) |
Blocks until a token is free or ctx is done; returns error. |
lim.WaitN(ctx, n) |
lim.WaitN(ctx, key, n) |
As above for n tokens. |
r := lim.Reserve(); r.Delay() |
res := lim.Allow(ctx, key); res.RetryAfter |
No Reservation object. Result.RetryAfter is the wait until this key is allowed again; Result.ResetAfter is time to a full bucket. There is no cancel-and-refund. |
lim.Limit() / lim.Burst() |
lim.Peek(ctx, key) → State{Limit, Remaining, ...} |
Peek reports state without consuming. |
lim.SetLimit(newR) / lim.SetBurst(newB) |
lim.SetLimit(capacity, refillRate) |
Single call updates both; existing per-key buckets are preserved (clamped down on shrink). |
| fractional cost | lim.AllowCost(ctx, key, cost float64) |
Consumes a non-integer cost all-or-nothing. |
ctx is a context.Context and key is a string. Allow/AllowN are
non-blocking; Wait/WaitN block.
Before / after¶
Global limiter (single stream):
// BEFORE — x/time/rate
lim := rate.NewLimiter(rate.Limit(100), 200) // 100 req/s, burst 200
if !lim.Allow() {
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
// AFTER — this library (single fixed key stands in for "global")
lim := tokenbucket.New(200, 100) // capacity 200, refill 100/s
defer lim.Close()
if !lim.Allow(ctx, "global").Allowed {
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
Per-user limiter (the case that needs a map with x/time/rate):
// BEFORE — x/time/rate: hand-rolled per-key map + mutex + eviction
type limiterSet struct {
mu sync.Mutex
m map[string]*rate.Limiter
}
func (s *limiterSet) get(user string) *rate.Limiter {
s.mu.Lock()
defer s.mu.Unlock()
l, ok := s.m[user]
if !ok {
l = rate.NewLimiter(rate.Limit(10), 20)
s.m[user] = l // NOTE: never evicted — grows forever
}
return l
}
// ... s.get(userID).Allow()
// AFTER — this library: keying is built in, idle keys auto-evicted
lim := tokenbucket.New(20, 10, tokenbucket.WithIdleCleanup(5*time.Minute))
defer lim.Close()
if !lim.Allow(ctx, "user:"+userID).Allowed {
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
Blocking wait:
// BEFORE
if err := lim.Wait(ctx); err != nil { return err }
// AFTER
if err := lim.Wait(ctx, "user:"+userID); err != nil { return err }
Notes on parity¶
- No
Reserve()/Reservation.x/time/rate's reservation model (reserve now, act later, optionally cancel and refund) has no direct equivalent. UseWait/WaitNto block, or readResult.RetryAfterfrom anAllowcall to schedule your own retry. There is no token refund. - Other algorithms share the interface. If you want smooth pacing rather
than bursty token-bucket behavior, swap
tokenbucket.Newforgcra.New(limit, burst, window)— theratelimit.Limiterinterface is identical, so call sites don't change. - Distributed. For a fleet-wide limit backed by Redis, swap the constructor
for
tokenbucket.NewDistributed(rate, capacity, store, prefix)(note: this distributed constructor takes rate first, capacity second — the opposite of the in-memoryNew). See the distributed recipe.
From sony/gobreaker¶
API mapping¶
gobreaker's Execute takes a func() (any, error) and returns
(any, error). This library's Execute takes a func(context.Context) error
and returns error — the value is carried out through a closure variable (see
the example below), which keeps the breaker allocation-free and generics-free.
sony/gobreaker |
This library (circuitbreaker) |
Notes |
|---|---|---|
gobreaker.NewCircuitBreaker(gobreaker.Settings{...}) |
circuitbreaker.New(circuitbreaker.Config{...}) |
Returns *CircuitBreaker. |
cb.Execute(func() (any, error) {...}) |
cb.Execute(ctx, func(ctx) error {...}) |
Context-aware; result via a captured variable. |
Settings.Name |
Config.Name |
Same. |
Settings.MaxRequests |
Config.HalfOpenMaxRequests |
Max concurrent probe requests in half-open (default 1). |
Settings.Interval (closed-state clearing window) |
Config.WindowDuration (with WindowType: TimeBased) |
Time-based failure window. Count-based (WindowType: CountBased, the default) uses Config.WindowSize instead. |
Settings.Timeout (open → half-open) |
Config.OpenTimeout |
How long the circuit stays open (default 30s). |
Settings.ReadyToTrip(counts) returning a bool |
Config.FailureThreshold (count-based) and/or Config.FailureRateThreshold + Config.MinimumRequests (time-based) |
Declarative thresholds instead of a callback. See mapping below. |
Settings.OnStateChange(name, from, to) |
Config.OnStateChange(name string, from, to State) |
Same shape. State is StateClosed / StateHalfOpen / StateOpen. |
Settings.IsSuccessful(err) bool |
Config.IsFailure(err) bool |
Inverted polarity: return true if the error counts as a failure. nil means all non-nil errors are failures. |
| (no equivalent) | Config.SuccessThreshold |
Consecutive half-open successes needed to close (default 1). |
| (no equivalent) | Config.RequestTimeout |
Per-Execute timeout; a timeout counts as a failure. |
cb.State() → gobreaker.State |
cb.State() → circuitbreaker.State |
StateClosed / StateHalfOpen / StateOpen. |
cb.Counts() |
cb.Snapshot() → Snapshot{Failures, Successes, Requests, FailureRate, ...} |
Point-in-time view. |
gobreaker.ErrOpenState |
circuitbreaker.ErrCircuitOpen |
errors.Is-comparable; wrapped in a *CircuitError. |
gobreaker.ErrTooManyRequests |
circuitbreaker.ErrTooManyRequests |
Half-open probe limit hit. |
Mapping ReadyToTrip¶
gobreaker trips via a callback over Counts. This library uses declarative
thresholds. Two common patterns:
// gobreaker: trip after 5 consecutive failures
ReadyToTrip: func(c gobreaker.Counts) bool {
return c.ConsecutiveFailures >= 5
}
// this library — count-based window (the default WindowType):
circuitbreaker.Config{
WindowType: circuitbreaker.CountBased,
WindowSize: 10, // ring buffer of the last N requests
FailureThreshold: 5, // open when failures in the window reach 5
}
// gobreaker: trip when >=60% of >=20 requests fail
ReadyToTrip: func(c gobreaker.Counts) bool {
return c.Requests >= 20 && float64(c.TotalFailures)/float64(c.Requests) >= 0.6
}
// this library — time-based window:
circuitbreaker.Config{
WindowType: circuitbreaker.TimeBased,
WindowDuration: 60 * time.Second,
BucketDuration: 1 * time.Second,
MinimumRequests: 20, // don't evaluate the rate below this volume
FailureRateThreshold: 0.6, // open at 60% failures
FailureThreshold: 1, // also require at least 1 raw failure
}
Before / after¶
// BEFORE — sony/gobreaker
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "payments",
MaxRequests: 1,
Interval: 0,
Timeout: 30 * time.Second,
ReadyToTrip: func(c gobreaker.Counts) bool {
return c.ConsecutiveFailures >= 5
},
OnStateChange: func(name string, from, to gobreaker.State) {
log.Printf("%s: %s -> %s", name, from, to)
},
})
body, err := cb.Execute(func() (any, error) {
return callPayments()
})
if err != nil {
if errors.Is(err, gobreaker.ErrOpenState) {
// fast-fail
}
return err
}
resp := body.(*Response)
// AFTER — this library
cb := circuitbreaker.New(circuitbreaker.Config{
Name: "payments",
WindowType: circuitbreaker.CountBased,
WindowSize: 10,
FailureThreshold: 5,
HalfOpenMaxRequests: 1,
OpenTimeout: 30 * time.Second,
OnStateChange: func(name string, from, to circuitbreaker.State) {
log.Printf("%s: %s -> %s", name, from, to)
},
})
// The result is carried out through a captured variable because Execute's fn
// returns only an error.
var resp *Response
err := cb.Execute(ctx, func(ctx context.Context) error {
var callErr error
resp, callErr = callPayments(ctx)
return callErr
})
if err != nil {
if errors.Is(err, circuitbreaker.ErrCircuitOpen) {
// fast-fail
}
return err
}
// use resp
Notes on parity¶
IsSuccessfulis inverted.gobreaker.Settings.IsSuccessfulreturnstruefor successes;Config.IsFailurereturnstruefor failures. By default all non-nil errors are failures, and callercontext.Canceledis not counted (unlike aRequestTimeout-induced deadline, which is).- Built-in fallback.
cb.ExecuteWithFallback(ctx, fn, fallback)runsfallback(ctx, err)whenfnfails or the circuit is open — no wrapper needed. For richer composition (breaker + retry + hedge) use the pipeline. - Registry.
circuitbreaker.NewRegistry()manages many named breakers if you were keeping amap[string]*gobreaker.CircuitBreaker. - Observability. Wire a
metric.RecorderviaConfig.WithRecorder(rec)to export Prometheus state/transition/latency series. See the observability recipe.
See also¶
- Recipe cookbook — copy-pasteable scenario recipes.
- docs/algorithms.md — algorithm theory and trade-offs.
- docs/distributed.md — Redis-backed limiters and fallback modes.