The admission middleware intercepts every request:
func admissionMiddleware(l *limiter.Limiter, timeout time.Duration) { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 1. Extract region and bucket from path info := router.PathFromContext(r.Context()) // 2. Check for X-Priority header priority := limiter.PriorityNormal if r.Header.Get("X-Priority") == "high" { priority = limiter.PriorityHigh } // 3. Request admission with timeout ctx, cancel := context.WithTimeout(r.Context(), timeout) defer cancel() ticket, err := l.Admit(ctx, limiter.Admission{ Region: info.Region, Bucket: info.Bucket, Priority: priority, }) // 4. Handle rejection or proceed if err != nil { w.Header().Set("Retry-After", ...) http.Error(w, "request rejected", 429) return } // 5. Proxy with assigned key index next.ServeHTTP(w, r.WithContext(withKeyIndex(ctx, ticket.KeyIndex))) }) }}
The admission timeout (configured via ADMISSION_TIMEOUT, default 5 minutes) is the maximum time a request can wait in the queue. After this, it’s rejected with a timeout error.
type bucketQueue struct { region string // e.g., "na1" bucket string // e.g., "na1:lol/summoner/v4/summoners/by-name/{name}" high []*admitRequest // High-priority queue normal []*admitRequest // Normal-priority queue wakeAt time.Time // Next scheduled dispatch time heapIndex int // Position in wake heap}
Queue Full Rejection (internal/limiter/limiter.go:163)
When a bucket’s queue reaches capacity:
if bucket.depth() >= l.cfg.QueueCapacity { // Pick the best key to estimate when capacity will be available _, earliest := l.pickKey(now, keys, bucket.region, bucket.bucket, priority) // Reject with calculated retry time return admitResponse{ err: &RejectedError{ Reason: "queue_full", RetryAfter: max(earliest.Sub(now), time.Second), }, }}
When the queue is full, RiftRelay rejects new requests immediately with 429 Too Many Requests and a Retry-After header. The retry time is based on when the rate limiter expects capacity to be available.
The dispatch function tries to admit queued requests:
func (l *Limiter) dispatch(bucket *bucketQueue, keys []keyState, wakeups *wakeHeap) { for { // 1. Dequeue next valid request (high priority first) req := bucket.dequeueValid() if req == nil { removeWake(wakeups, bucket) // No more requests return } // 2. Pick the best key keyIndex, earliest := l.pickKey(now, keys, bucket.region, bucket.bucket, req.admission.Priority) if keyIndex < 0 { // No key available at all req.resp <- admitResponse{err: &RejectedError{Reason: "no_available_key"}} continue } // 3. Check if ready now if earliest.After(now) { // Not ready yet - put request back and schedule wakeup bucket.enqueue(req) // Back to front of queue upsertWake(wakeups, bucket, earliest) return } // 4. Try to consume rate limit key := &keys[keyIndex] if !key.app(region).consume(now) || !key.method(bucket).consume(now) { // Race condition or rounding error - retry soon bucket.enqueue(req) upsertWake(wakeups, bucket, now.Add(5*time.Millisecond)) return } // 5. Grant admission! req.resp <- admitResponse{ticket: Ticket{KeyIndex: keyIndex}} // Continue to next request in queue }}
The dispatch loop processes all ready requests in a bucket before moving to the next event. This maximizes throughput when multiple requests can be admitted at once.
Requests can be rejected for several reasons (internal/limiter/types.go:52):
queue_full
The bucket’s queue has reached QUEUE_CAPACITY. The client should back off and retry after the suggested Retry-After time.See internal/limiter/limiter.go:167.
no_available_key
None of the configured API keys can handle the request (e.g., all keys are rate-limited). This is rare with proper configuration.See internal/limiter/limiter.go:236.
shutting_down
RiftRelay is shutting down and draining the queue. The request should be retried against a different instance or after restart.See internal/limiter/limiter.go:128.
invalid_route
The request path couldn’t be parsed (missing region or upstream path). This indicates a client error.See internal/limiter/limiter.go:45.
All rejections return RejectedError with a Reason and optional RetryAfter duration:
type RejectedError struct { Reason string RetryAfter time.Duration}
When RiftRelay receives SIGINT or SIGTERM (main.go:24):
The HTTP server stops accepting new connections
Existing requests in the admission queue complete or time out within SHUTDOWN_TIMEOUT
After the timeout, remaining queued requests are rejected with shutting_down (internal/limiter/limiter.go:124)
case done := <-l.closeCh: for _, bucket := range buckets { for req := bucket.dequeueValid(); req != nil; req = bucket.dequeueValid() { select { case req.resp <- admitResponse{err: &RejectedError{Reason: "shutting_down"}}: default: } } } close(done) return
Set SHUTDOWN_TIMEOUT high enough to allow normal traffic to drain. The default is 20 seconds, which may not be sufficient for heavily queued workloads.
Set QUEUE_CAPACITY based on expected burst traffic. Default is 2048, which works for most use cases. If you see frequent queue_full rejections, increase this value.
2
Use high priority sparingly
The X-Priority: high header bypasses pacing but can cause bursty traffic. Reserve it for time-sensitive requests like game events.
3
Set realistic admission timeouts
The default ADMISSION_TIMEOUT of 5 minutes is generous. For interactive use cases, consider lowering it to fail fast instead of queuing for extended periods.
4
Monitor queue depth metrics
If queue depth consistently grows, you may need more API keys or need to reduce request rate upstream.