ratelimit

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

README

ratelimit

Small, reusable rate-limiting primitives built on golang.org/x/time/rate.

This package knows nothing about HTTP, Tumblebug, or Beetle. It provides the two ways a rate limit can be enforced; callers wire them up and translate the results into their own responses.

Type Strategy Question it answers Typical use
Pacer Shaping — delay the excess "May anyone call this shared resource right now?" Stay under a downstream API's global rate limit
Cooldown Policing — reject the excess "May this specific key act again yet?" Stop a user from polling one resource too often

Rule of thumb: shape calls you make to someone else (you control the pace, so waiting is better than failing); police calls others make to you (you can't slow them down, so refuse and tell them when to come back).

Where each one sits

The two guard opposite edges of a process and are used independently — separate instances, no shared state, separate configuration. Using one never implies the other. Beetle happens to use both, one at each edge:

               inbound                             outbound
  clients ---> [ Cooldown ] ---> Beetle logic ---> [ Pacer ] ---> CB-Tumblebug
               reject a repeat                     delay until    (2 req/s)
               that came too soon                  a slot frees up
  • Cooldown faces the callers. Beetle cannot slow a client down, so an early repeat is refused outright (HTTP 429 + Retry-After). It protects Beetle and the infrastructure behind it from a client polling harder than intended.
  • Pacer faces the subsystems Beetle calls. Here Beetle is the one being restrained, and it controls its own send rate, so a call waits its turn instead of failing. It fails (HTTP 503 + Retry-After) only when the wait budget runs out before a slot opens.

Pacer

Pacer shapes traffic: it spreads concurrent callers over time so a shared downstream resource is never called faster than a fixed interval. Callers are admitted one at a time, in arrival order.

[callers] -> [token bucket: 1 call per interval] -> [resource]
pacer := ratelimit.NewPacer(600 * time.Millisecond) // ~1.67 calls/s

// Wait blocks until this caller's slot arrives, bounded by the context.
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()

if err := pacer.Wait(ctx); err != nil {
    return err // *ErrLimited if the slot lands after the deadline
}
resp, err := callDownstream()

Wait reserves a slot up front, so waits are bounded and fair. If the reserved slot would land after the context deadline, it fails immediately with *ErrLimited and releases the reservation, instead of holding the caller for the full budget only to time out.

The interval is fixed. Transient rate limiting from the downstream resource is better handled by retrying the affected request (for example, an HTTP client retry on 429) than by slowing every caller down.

Cooldown

Cooldown polices traffic: it enforces "at most one action per key per interval" and rejects anything that arrives early instead of delaying it. Each key gets its own limiter with a burst of 1; unused keys are evicted in the background so memory stays bounded.

cd := ratelimit.NewCooldown(
    3*time.Minute,  // interval: one call per key every 3 minutes
    1*time.Hour,    // maxAge: evict keys idle this long
    10*time.Minute, // cleanupInterval: how often to evict
)
defer cd.Stop()

if allowed, retryAfter := cd.Allow(nsId + ":" + infraId); !allowed {
    return fmt.Errorf("retry after %v", retryAfter)
}

A rejected call does not consume the key's slot, so retryAfter stays accurate no matter how often a client retries.

Pass 0 for maxAge or cleanupInterval to skip the background goroutine entirely.

ErrLimited

Pacer.Wait returns *ErrLimited when it gives up. It carries RetryAfter, the duration to advertise to the caller. Two helpers cover the usual response wiring:

if retryAfter, ok := ratelimit.RetryAfter(err); ok {
    w.Header().Set("Retry-After", strconv.Itoa(ratelimit.RetryAfterSeconds(retryAfter)))
    w.WriteHeader(http.StatusServiceUnavailable)
}
  • RetryAfter(err) — unwraps any *ErrLimited in the error chain.
  • RetryAfterSeconds(d) — rounds up to whole seconds, never below 1, as Retry-After requires.

Cooldown.Allow returns retryAfter directly instead of an error, since a rejected call is a normal outcome there rather than a failure.

Concurrency

Both types are safe for concurrent use. A Pacer or Cooldown is meant to be created once and shared by every caller that contends for the same resource — one per resource, not one per request.

On the names

Pacing and cooldown are the established terms for these two policies, so the types borrow them rather than inventing anything. golang.org/x/time/rate exposes the same two modes as Wait and Allow, which is exactly the method each type specializes in.

The -er asymmetry is intentional. Go reserves that suffix for actors, mainly single-method interfaces (io.Reader); concrete types are named for what they are (sync.Once, sync.WaitGroup, semaphore.Weighted). A Pacer is an actor — it paces your calls for you. A Cooldown is a rule you consult (cooldown.Allow(key)), so Cooldowner would apply the letter of a convention that doesn't fit.

Users in this repository

Documentation

Overview

Package ratelimit provides small, reusable rate-limiting primitives built on top of the standard golang.org/x/time/rate token-bucket limiter.

Rate limiting is enforced in one of two ways, and this package offers one type for each:

  • Pacer shapes traffic: it delays calls so a resource with a known rate limit (e.g. an API capped at N req/s) is never called faster than that limit. Use it on calls you make to someone else.
  • Cooldown polices traffic: it rejects a repeat action on the same key that arrives before its interval has elapsed. Use it on calls others make to you.

Neither type is tied to any particular transport or caller; wire them up where needed and translate the result into whatever response shape (HTTP 503/429 + Retry-After, etc.) is appropriate for that caller.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RetryAfter

func RetryAfter(err error) (retryAfter time.Duration, ok bool)

RetryAfter reports whether err is (or wraps) an ErrLimited and, if so, how long the caller should wait. It lets any package translate a rate-limit failure into its own response without importing whichever package produced the error.

func RetryAfterSeconds

func RetryAfterSeconds(d time.Duration) int

RetryAfterSeconds converts d into whole seconds for a Retry-After header, rounding up so the value never advertises a shorter wait than the limiter actually enforces.

Types

type Cooldown

type Cooldown struct {
	// contains filtered or unexported fields
}

Cooldown rejects repeated actions on the same key that arrive less than interval apart (e.g. "this API may be called for a given resource once every N minutes"). Unlike Pacer it never delays a caller: an action is either allowed now or refused with the wait time. It is implemented as one rate.Limiter (burst 1) per key.

func NewCooldown

func NewCooldown(interval, maxAge, cleanupInterval time.Duration) *Cooldown

NewCooldown creates a Cooldown enforcing interval between allowed calls per key. Entries unused for longer than maxAge are periodically evicted (checked every cleanupInterval) to bound memory; pass maxAge or cleanupInterval as 0 to disable cleanup (e.g. for short-lived Cooldowns such as in tests).

func (*Cooldown) Allow

func (c *Cooldown) Allow(key string) (allowed bool, retryAfter time.Duration)

Allow reports whether an action for key is allowed now. If not, retryAfter is the duration to wait before the next allowed call.

func (*Cooldown) Stop

func (c *Cooldown) Stop()

Stop stops the background cleanup goroutine, if any.

type ErrLimited

type ErrLimited struct {
	RetryAfter time.Duration
}

ErrLimited reports that a call was refused because a rate limit would otherwise be exceeded. RetryAfter is how long the caller should wait before trying again.

func (*ErrLimited) Error

func (e *ErrLimited) Error() string

type Pacer

type Pacer struct {
	// contains filtered or unexported fields
}

Pacer spaces out calls to a resource with a fixed, known rate limit. Callers wait their turn rather than being rejected, so a burst is spread over time instead of failing.

Architecture: [callers] -> [token bucket: one call per interval] -> [resource]

func NewPacer

func NewPacer(interval time.Duration) *Pacer

NewPacer creates a Pacer admitting one call per interval.

func (*Pacer) Interval

func (p *Pacer) Interval() time.Duration

Interval returns the delay enforced between admitted calls.

func (*Pacer) Wait

func (p *Pacer) Wait(ctx context.Context) error

Wait blocks until the caller's slot comes up. It returns *ErrLimited immediately, without waiting, when that slot would arrive after ctx's deadline, and ctx.Err() when ctx is canceled while waiting.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL