ratelimiter

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2026 License: MIT Imports: 4 Imported by: 1

README

rate-limiter

codecov Go Reference

A highly concurrent, thread-safe Go package for rate limiting resource consumption. Features granular delays, exponential backoff, and jitter support. Zero external dependencies.

Features

  • Concurrent Engine: Read/Write locks engineered to handle high throughput without unnecessary lock contention
  • Granular Tuning: Set separate delays per specific resource alongside global base delays
  • Exponential Backoff: Dynamically slow down resource firing using proven backoff math
  • Jitter Support: Add randomness to delays to help avoid thundering herd and collision problems
  • Clean Configuration: Intuitive functional options API with sensible defaults
  • Context Awareness: Full context.Context integration for graceful cancellation, eliminating goroutine leaks
  • Debug Logging: Optional logging interface to trace rate limiting activity
  • Zero Dependencies: Relies solely on the Go standard library

Installation

go get github.com/rohmanhakim/rate-limiter

Quick Start

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/rohmanhakim/rate-limiter"
)

func main() {
	// 1. Create a concurrent rate limiter with sensible defaults
	rl := ratelimiter.NewConcurrentRateLimiter(
		ratelimiter.WithJitter(100*time.Millisecond),
		ratelimiter.WithInitialDuration(1*time.Second),
		ratelimiter.WithMultiplier(2.0),
		ratelimiter.WithMaxDuration(30*time.Second),
	)

	// Set a global base delay (all requests will wait AT LEAST this long)
	rl.SetBaseDelay(500 * time.Millisecond)

	ctx := context.Background()
	resource := "api.example.com"

	// 2. Wrap your operations by waiting
	err := rl.Wait(ctx, resource)
	if err != nil { // handles context deadline/cancellation
		fmt.Printf("Rate limit waiter cancelled: %v\n", err)
		return
	}

	fmt.Printf("Executing request for %s...\n", resource)
	// Output: Executing request for api.example.com...
}

Configuration Options

Functional Options

The NewConcurrentRateLimiter function accepts functional options for configuration:

Option Description Default
WithJitter(d time.Duration) Maximum random delay added to compute jitter 0 (no jitter)
WithInitialDuration(d time.Duration) Initial backoff duration 1 second
WithMultiplier(m float64) Exponential backoff multiplier 2.0
WithMaxDuration(d time.Duration) Cap for maximum backoff duration 1 minute
WithDebugLogger(l DebugLogger, ...) Sets custom logging implementation NoOpLogger
WithLogAttrs(attrs ...any) Additional attributes passed to the logger none
Additional Limits & Overrides

You can manually adjust delays for specific resources overriding the base settings dynamically:

rl := ratelimiter.NewConcurrentRateLimiter()

// Global rate (e.g. 1 req/s)
rl.SetBaseDelay(1 * time.Second) 

// Specific resource override (e.g. 1 req / 5s for stricter endpoints)
rl.SetResourceDelay("strict.example.com", 5 * time.Second) 

Backoff Mechanism

The rate limiter supports explicit signal-driven exponential backoffs. If a remote server sends a 429 Too Many Requests or 5xx error, you can instruct the limiter to back off:

// Trigger an exponential backoff state specifically for "api.example.com"
rl.Backoff(ctx, "api.example.com")

// Optionally define a server-suggested delay (e.g., from Retry-After header)
rl.Backoff(ctx, "api.example.com", ratelimiter.BackoffOptions{
    ServerDelay: 5 * time.Second,
})

// Once the remote service recovers, reset the backoff state
rl.ResetBackoff("api.example.com")

The computed delay resolution logic takes the highest active duration among: BaseDelay, ResourceDelay, and BackoffDelay before adding randomized Jitter.

Context Cancellation

The rate limiter handles context cancellation gracefully across delays, ensuring there are no hidden memory leaks during high concurrency and long backoff bounds:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

// If the delay is 5s, it will terminate after 2s without leaking resources
err := rl.Wait(ctx, "slow.api.com")
if err != nil {
    // Expected: context deadline exceeded
}

Debug Logging

Adding observability to rate limits and backoffs is simple via the DebugLogger interface.

type MyLogger struct{}

func (l *MyLogger) Enabled() bool { return true }

func (l *MyLogger) LogBackoff(ctx context.Context, resource string, count int, delay time.Duration, attrs ...any) {
    log.Printf("Backoff triggered [%s]: attempt=%d, delay=%v", resource, count, delay)
}

func (l *MyLogger) LogRateLimit(ctx context.Context, resource string, delay time.Duration, reason ratelimiter.RateLimitReason, attrs ...any) {
    log.Printf("Rate limit applied [%s]: delay=%v, reason=%s", resource, delay, reason)
}

// Attach it to the rate limiter!
rl := ratelimiter.NewConcurrentRateLimiter(
    ratelimiter.WithDebugLogger(&MyLogger{}),
)

API Reference

Key Types & Methods
// Core Interface for Mocking
type RateLimiter interface { ... }

// The primary implementation
type ConcurrentRateLimiter struct { ... }

func NewConcurrentRateLimiter(opts ...RateLimiterOption) *ConcurrentRateLimiter

// Setting Global & Specific Rules
func (r *ConcurrentRateLimiter) SetBaseDelay(baseDelay time.Duration)
func (r *ConcurrentRateLimiter) SetJitter(jitter time.Duration)
func (r *ConcurrentRateLimiter) SetResourceDelay(resource string, delay time.Duration)

// Execution
func (r *ConcurrentRateLimiter) Wait(ctx context.Context, resource string) error
func (r *ConcurrentRateLimiter) Backoff(ctx context.Context, resource string, opts ...BackoffOptions)
func (r *ConcurrentRateLimiter) ResetBackoff(resource string)
func (r *ConcurrentRateLimiter) ResolveDelay(ctx context.Context, resource string) time.Duration

type BackoffOptions struct {
	ServerDelay time.Duration
}

Examples

Complete, runnable examples are provided in the example/ directory demonstrating patterns such as:

  • multi_service_fetch/: A complex, concurrent worker pool crawling multiple domains applying domain-specific delays and respecting backoff rules.
  • simple_fetch/: A straightforward single pipeline demonstrating basic throttling.

License

MIT License - see LICENSE for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BackoffOptions

type BackoffOptions struct {
	// ServerDelay is an optional server-suggested delay (e.g., from Retry-After header).
	ServerDelay time.Duration
}

BackoffOptions configures backoff behavior.

type ConcurrentRateLimiter

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

func NewConcurrentRateLimiter

func NewConcurrentRateLimiter(opts ...RateLimiterOption) *ConcurrentRateLimiter

func (*ConcurrentRateLimiter) Backoff

func (r *ConcurrentRateLimiter) Backoff(ctx context.Context, resource string, opts ...BackoffOptions)

Backoff triggers exponential backoff for the given resource. It increments the backoff counter and computes the delay. The optional opts parameter allows the caller to provide a server-suggested delay (e.g., from Retry-After header or response body). The initial backoff will be max(serverDelay, initialDuration).

func (*ConcurrentRateLimiter) BaseDelay

func (r *ConcurrentRateLimiter) BaseDelay() time.Duration

func (*ConcurrentRateLimiter) Jitter

func (r *ConcurrentRateLimiter) Jitter() time.Duration

func (*ConcurrentRateLimiter) ResetBackoff

func (r *ConcurrentRateLimiter) ResetBackoff(host string)

ResetBackoff resets the backoff counter for the given host. Called after a successful request to clear backoff state.

func (*ConcurrentRateLimiter) ResolveDelay

func (r *ConcurrentRateLimiter) ResolveDelay(ctx context.Context, host string) time.Duration

Compute the final delay resolution for given host FinalDelay = max(BaseDelay, crawlDelay, BackoffDelay) + Jitter

func (*ConcurrentRateLimiter) ResourceTimings

func (r *ConcurrentRateLimiter) ResourceTimings() map[string]resourceTiming

func (*ConcurrentRateLimiter) SetBaseDelay

func (r *ConcurrentRateLimiter) SetBaseDelay(baseDelay time.Duration)

func (*ConcurrentRateLimiter) SetDebugLogger

func (r *ConcurrentRateLimiter) SetDebugLogger(logger DebugLogger)

SetDebugLogger sets the debug logger for the rate limiter. This allows users to provide custom logging implementation.

func (*ConcurrentRateLimiter) SetJitter

func (r *ConcurrentRateLimiter) SetJitter(jitter time.Duration)

func (*ConcurrentRateLimiter) SetResourceDelay

func (r *ConcurrentRateLimiter) SetResourceDelay(resource string, delay time.Duration)

Set delay to given resource, separated from global base delay

func (*ConcurrentRateLimiter) Wait

func (r *ConcurrentRateLimiter) Wait(ctx context.Context, resource string) error

Wait blocks until the resource is ready to be consumed. It resolves the delay, waits if necessary, and marks the last consumed time. Returns nil when ready to proceed, or error if context is cancelled.

type DebugLogger

type DebugLogger interface {
	// Enabled returns true if debug logging is enabled.
	// When false, the retry handler will skip logging entirely for efficiency.
	Enabled() bool

	LogBackoff(ctx context.Context, resource string, count int, delay time.Duration, attrs ...any)

	LogRateLimit(ctx context.Context, resource string, delay time.Duration, reason RateLimitReason, attrs ...any)
}

DebugLogger provides structured debug logging capabilities for the rate limiter package. Users can implement this interface to provide custom logging behavior. When debug mode is disabled, use NoOpLogger for zero overhead.

type NoOpLogger

type NoOpLogger struct{}

NoOpLogger is a no-operation implementation of DebugLogger. It provides zero overhead when debug mode is disabled. All methods are empty and Enabled() always returns false.

func NewNoOpLogger

func NewNoOpLogger() *NoOpLogger

NewNoOpLogger creates a new NoOpLogger instance.

func (*NoOpLogger) Enabled

func (n *NoOpLogger) Enabled() bool

Enabled returns false - debug logging is disabled.

func (*NoOpLogger) LogBackoff

func (n *NoOpLogger) LogBackoff(_ context.Context, _ string, _ int, _ time.Duration, _ ...any)

LogBackoff is no-op.

func (*NoOpLogger) LogRateLimit

func (n *NoOpLogger) LogRateLimit(_ context.Context, _ string, _ time.Duration, _ RateLimitReason, _ ...any)

LogRetry is a no-op.

type RateLimitReason

type RateLimitReason string

RateLimitReason describes why a rate limit was applied.

const (
	// RateLimitReasonBaseDelay indicates the base delay was applied.
	RateLimitReasonBaseDelay RateLimitReason = "base_delay"
	// RateLimitReasonCrawlDelay indicates crawl-delay from robots.txt was applied.
	RateLimitReasonConsumeDelay RateLimitReason = "consume_delay"
	// RateLimitReasonBackoff indicates exponential backoff was applied.
	RateLimitReasonBackoff RateLimitReason = "backoff"
)

type RateLimiter

type RateLimiter interface {
	SetBaseDelay(baseDelay time.Duration)
	SetJitter(jitter time.Duration)
	SetDebugLogger(logger DebugLogger)
	SetResourceDelay(resource string, delay time.Duration)
	Backoff(ctx context.Context, resource string, opts ...BackoffOptions)
	ResetBackoff(resource string)
	Wait(ctx context.Context, resource string) error
	ResolveDelay(ctx context.Context, resource string) time.Duration
}

RateLimiter Specialized component to manage rate limiting during resource consumption Responsibilities: - Bookkeep each reasource's last consumed timestamp - Compute the final delay for each resource given various factors - Make sure the consumption process respect the resource's policy

type RateLimiterOption

type RateLimiterOption func(*rateLimiterConfig)

RateLimiterOption is a functional option for configuring retry behavior.

func WithDebugLogger

func WithDebugLogger(debugLogger DebugLogger, attrs ...any) RateLimiterOption

func WithInitialDuration

func WithInitialDuration(d time.Duration) RateLimiterOption

WithInitialDuration sets the initial backoff duration. Default is 1 second.

func WithJitter

func WithJitter(d time.Duration) RateLimiterOption

WithJitter sets the maximum random duration added to backoff delays. This helps avoid thundering herd problems. Default is 0 (no jitter).

func WithLogAttrs

func WithLogAttrs(attrs ...any) RateLimiterOption

WithLogAttrs sets additional attributes to be passed to the logger. Attributes follow Go's slog convention for structured logging - alternating key-value pairs (string, any, string, any, ...). These attributes are passed to LogRetry calls for structured logging context.

func WithMaxDuration

func WithMaxDuration(d time.Duration) RateLimiterOption

WithMaxDuration sets the maximum backoff duration. Default is 1 minute.

func WithMultiplier

func WithMultiplier(m float64) RateLimiterOption

WithMultiplier sets the backoff multiplier. Each subsequent delay is multiplied by this value. Default is 2.0.

Directories

Path Synopsis
example
multi_service_fetch command
Package main contains a fake HTTP server for demonstrating the rate limiter package.
Package main contains a fake HTTP server for demonstrating the rate limiter package.
simple_fetch command
Package main demonstrates a simple rate-limited HTTP client.
Package main demonstrates a simple rate-limited HTTP client.
simple_fetch/simple_logger
Package simple_logger provides a simple implementation of the ratelimiter.DebugLogger interface that prints retry information to stdout.
Package simple_logger provides a simple implementation of the ratelimiter.DebugLogger interface that prints retry information to stdout.

Jump to

Keyboard shortcuts

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