Skip to content

REST Rate Limits ​

Overview ​

Every REST call participates in ratelimit.Limiter. The client uses the path without its query string as the initial route key, then adopts Discord's returned bucket hash so routes sharing a server bucket coordinate. Global resets and the process-wide request window are handled by the limiter.

Architecture ​

The request sequence is: wait for the route and global limits, send the request, parse X-RateLimit-* headers, and update the limiter. A 429 response is special: REST reads JSON retry_after first, then the Retry-After header, then ResetAfter, waits with the caller context, and repeats the request. A custom ratelimit.Store can persist BucketState across client instances; the built-in memory store is process-local.

Client.MaxRetries bounds the 429 retry loop. When set to a positive value, the client gives up after that many retries and returns *RateLimitError instead of retrying forever; zero (the default) keeps the historical unbounded behavior. Every 429 honors this budget, including responses without a usable retry_after hint (for example a Cloudflare-fronted HTML 429); in that case the client backs off one second between attempts. Client.BucketState(route) exposes the limiter's current state for a route so callers can inspect remaining budget and reset time without guessing.

Quick Start ​

go
package main

import (
	"fmt"
	"net/http"

	"github.com/discord-go/discord.go/ratelimit"
)

func main() {
	header := make(http.Header)
	header.Set("X-RateLimit-Bucket", "abc")
	header.Set("X-RateLimit-Remaining", "4")
	header.Set("X-RateLimit-Reset-After", "1.5")
	info := ratelimit.ParseHeaders(header)
	store := ratelimit.NewMemoryStore()
	limiter := ratelimit.NewLimiter(store)
	limiter.Update("GET:/channels/1/messages", info)
	state, ok := store.Get("bucket:" + info.Bucket)
	fmt.Println(ok, state.Remaining, info.ResetAfter)
}

Using A Limiter ​

NewLimiter(nil) creates a memory-backed limiter. Use Wait(ctx, bucket) just before a request and Update(bucket, ratelimit.ParseHeaders(resp.Header)) afterward. An unknown bucket proceeds immediately. Once a hash is returned, the route is mapped to bucket:<hash> and subsequent calls use that state.

Common Patterns ​

Share a limiter for all REST clients using one token. Use ResetAfter for relative server timing and Reset when an absolute server time is available. Pass a deadline-bearing context so a shutdown or user request can cancel a wait. For distributed workers, implement Store over a shared atomic data store and account for clock skew.

Best Practices ​

Let REST own 429 waiting rather than adding an uncoordinated sleep around each call. Keep route keys stable and never key on a full URL with access tokens. Monitor remaining and reset values, but treat them as server hints rather than permission to burst beyond a known global policy.

Common Mistakes ​

The default memory limiter does not coordinate separate processes. A route key with query parameters is normalized by REST requests but not by a direct limiter caller. Retry-After may be fractional seconds. A context canceled while waiting returns the context error and does not send the request.

API Walkthrough ​

The REST-facing API is the ratelimit.Limiter interface plus NewLimiter, Client.MaxRetries, Client.BucketState, and RateLimitError; the underlying ../ratelimit/ package provides Store, MemoryStore, BucketState, Info, ParseHeaders, and all storage methods.

Examples ​

The Quick Start program is complete and runnable and demonstrates bucket hash adoption without HTTP.

Released under the Apache License 2.0.