Go by Example

Rate Limiting

Control the rate of operations using tickers and the golang.org/x/time/rate token bucket.

Rate limiting controls how frequently an operation can proceed. Go's standard approach uses a time.Ticker as a simple token source, or the golang.org/x/time/rate package for full token-bucket semantics with burst support.

A basic rate limiter uses a ticker channel. Each tick grants permission for one request. The sender blocks until the next tick fires, naturally enforcing the rate.

package main
 
import (
    "fmt"
    "time"
)
 
func main() {
    requests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        requests <- i
    }
    close(requests)
 
    // allow 1 request per 200ms
    limiter := time.NewTicker(200 * time.Millisecond)
    defer limiter.Stop()
 
    for req := range requests {
        <-limiter.C
        fmt.Println("request", req, time.Now().Format("15:04:05.000"))
    }
}

For burst allowances, use golang.org/x/time/rate. A Limiter with rate r and burst b permits up to b events at once and then allows r events per second:

import "golang.org/x/time/rate"
 
limiter := rate.NewLimiter(rate.Every(100*time.Millisecond), 3) // 10/s, burst of 3
 
for i := 0; i < 10; i++ {
    if err := limiter.Wait(ctx); err != nil {
        return err
    }
    process(i)
}

limiter.Allow() is non-blocking - useful when you want to drop rather than queue:

if !limiter.Allow() {
    http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
    return
}

In production

The time/rate package implements a token bucket that supports burst allowances. Use it at the service boundary to protect downstream dependencies. For multi-instance deployments, local rate limiting is not sufficient - you need a distributed counter (Redis INCR with TTL, or a sidecar rate limiter like Envoy).

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free