feat(server): cap the request rate per caller - #19
Merged
Conversation
ekalinin
force-pushed
the
chore/deps-and-docs
branch
from
August 17, 2026 19:26
0924278 to
e30b7c6
Compare
ekalinin
force-pushed
the
feat/rate-limit
branch
2 times, most recently
from
August 17, 2026 19:35
7935cbd to
9a30b32
Compare
ekalinin
force-pushed
the
chore/deps-and-docs
branch
from
August 17, 2026 19:35
e30b7c6 to
772c3f3
Compare
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
force-pushed
the
chore/deps-and-docs
branch
from
August 18, 2026 09:47
772c3f3 to
55d41b2
Compare
ekalinin
force-pushed
the
feat/rate-limit
branch
from
August 18, 2026 09:47
9a30b32 to
f7f9299
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
defaults.max_concurrent_queriessemaphore 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 theClientIPFrom*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
/healthzand/readyzprobes 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: 0disables 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, 429while the probes keep answering 200.Verification
go test -race ./...,golangci-lint run ./...- clean.