Skip to content

feat(server): cap the request rate per caller - #19

Merged
ekalinin merged 1 commit into
chore/deps-and-docsfrom
feat/rate-limit
Aug 20, 2026
Merged

feat(server): cap the request rate per caller#19
ekalinin merged 1 commit into
chore/deps-and-docsfrom
feat/rate-limit

Conversation

@ekalinin

@ekalinin ekalinin commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Stacked on #18.

Problem

The defaults.max_concurrent_queries semaphore bounds how many queries run at once, but nothing bounded how fast they could be submitted. One client could still churn through pool connections, storage writes and idempotency keys as fast as the process could accept or reject them.

Change

server.rate_limit.{requests_per_second,burst} gives each caller a token bucket. The key is the authenticated subject where there is one, and the resolved client address otherwise - the address that the ClientIPFrom* middleware produced, so it already accounts for the configured number of trusted proxy hops.

It runs on both transports: a chi middleware in REST and an interceptor in Connect that covers unary and streaming calls.

Idle buckets are evicted, so the map cannot grow without bound on a busy or hostile deployment; the sweep is amortized to at most once per eviction window.

The /healthz and /readyz probes are exempt: the kubelet calls them on a schedule and must neither be starved by a noisy caller nor consume that caller's budget.

requests_per_second: 0 disables the limit, and startup logs a warning in that case so an absent limit is a visible decision rather than an oversight.

Tests

The bucket admits the burst and refuses the next request; budgets do not bleed between keys; a disabled limiter admits everything; the key prefers the subject over the address; in REST four requests give 200, 200, 429, 429 while the probes keep answering 200.

Verification

go test -race ./..., golangci-lint run ./... - clean.

The concurrency semaphore bounds how many queries run at once, but nothing
bounded how fast they could be submitted, so one client could still churn
through pool connections, storage writes and idempotency keys as fast as the
process could accept or reject them.

server.rate_limit gives each caller a token bucket, keyed by authenticated
subject where there is one and by the resolved client address otherwise, on
both REST and Connect. Idle buckets are evicted so the map cannot grow
without bound. The health probes are exempt: the kubelet calls them on a
schedule and must not be starved by a noisy caller, nor consume its budget.
@ekalinin
ekalinin force-pushed the chore/deps-and-docs branch from 772c3f3 to 55d41b2 Compare August 18, 2026 09:47
@ekalinin
ekalinin merged commit 9b25c5e into chore/deps-and-docs Aug 20, 2026
6 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant