Skip to content

Rate Limits ​

Overview ​

The ratelimit package coordinates REST requests across Discord route buckets and the global limit. Limiter blocks until a request can proceed, while Store persists bucket state. MemoryStore is concurrency-safe and is the default when NewLimiter(nil) is used.

Architecture ​

Info is parsed from response headers: bucket hash, remaining requests, reset time, reset-after duration, global status, and scope. ParseHeaders reads X-RateLimit-Bucket, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Reset-After, and X-RateLimit-Global. BucketState stores only remaining and reset time.

NewLimiter(store) creates a limiter. It enforces a 50-request-per-second process-wide global limit using golang.org/x/time/rate.Limiter (a token bucket) and tracks route locks. The token bucket allows concurrent goroutines to reserve tokens in parallel without serializing through a global mutex. Once a response supplies a bucket hash, the route is mapped to the hash so aliases share state. Wait honors context cancellation while sleeping for global or bucket resets; unknown buckets proceed immediately. Update records the parsed response and global reset.

Quick Start ​

go
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

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

func main() {
	header := make(http.Header)
	header.Set("X-RateLimit-Bucket", "messages")
	header.Set("X-RateLimit-Remaining", "2")
	header.Set("X-RateLimit-Reset-After", "0.25")
	info := ratelimit.ParseHeaders(header)

	limiter := ratelimit.NewLimiter(ratelimit.NewMemoryStore())
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := limiter.Wait(ctx, "/channels/1/messages"); err != nil {
		panic(err)
	}
	limiter.Update("/channels/1/messages", info)
	fmt.Println(info.Bucket, info.Remaining)
}

Creating A Limiter ​

Store has Get(bucketID) (BucketState, bool) and Put(bucketID, state). Implement it with shared storage when multiple processes need coordinated limits. The in-memory store is process-local. NewLimiter accepts nil and creates an in-memory store, so a custom store is only needed for durability or distribution.

Using Buckets ​

Call Wait immediately before the HTTP request and Update after parsing its headers. Use a stable route key, usually the REST path without query parameters. The REST client does this itself. A 429 response should supply Retry-After or a JSON retry interval at the transport layer; the limiter's header state prevents subsequent calls from racing the reset.

Common Patterns ​

Use ParseHeaders(resp.Header) and pass the result to Update. Store reset times using the server values when available and use ResetAfter for relative responses. Pass a context with a deadline to Wait so shutdown cannot leave a goroutine asleep until a long reset.

Best Practices ​

Share one limiter among REST clients that share a token and process. Keep route keys consistent. Treat the memory limiter as coordination, not a guarantee against a server-side limit; Discord can change buckets and scopes.

Common Mistakes ​

Do not call Wait with an empty key and expect per-route coordination; the global window still applies, but no bucket state is stored. Do not use a route path that includes volatile query parameters as a permanent bucket key. Do not ignore context.Canceled from a blocked wait.

API Walkthrough ​

The public API is Limiter, Store, BucketState, Info, ParseHeaders, NewLimiter, MemoryStore, NewMemoryStore, MemoryStore.Get, and MemoryStore.Put.

Examples ​

The Quick Start program is complete and runnable without a Discord request. REST integration details are in ../rest/ratelimits.md.

Released under the Apache License 2.0.