Skip to content

limiter: Short-circuit when no rates or connections are configured#63751

Draft
juliaogris wants to merge 3 commits intomasterfrom
julia/limiter
Draft

limiter: Short-circuit when no rates or connections are configured#63751
juliaogris wants to merge 3 commits intomasterfrom
julia/limiter

Conversation

@juliaogris
Copy link
Contributor

@juliaogris juliaogris commented Feb 12, 2026

Every Teleport service creates a Limiter unconditionally, even when no
connection_limits are set. Previously, NewRateLimiter injected a fake
default rate of 100M req/s to satisfy a non-empty-rates invariant in
TokenLimiter. This meant every request still paid the full cost of the
rate limiting machinery - extract client IP, acquire mutex, FnCache
lookup, token bucket consume - all for a rate that would never reject.

This PR removes the fake default rate and adds early returns so the
limiter honestly does nothing when no limits are configured:
RateLimiter.RegisterRequest returns immediately when the effective rate
set is empty, TokenLimiter.ServeHTTP skips IP extraction and rate
consumption, and ConnectionsLimiter skips tracking when
maxConnections is zero.

This is primarily a code clarity change. Micro-benchmarks show the noop
path is 5-160x faster in isolation, but a macro-benchmark through a real
TCP HTTP server shows no measurable difference in end-to-end throughput -
the limiter's ~300ns is noise against the ~9500ns of TCP/HTTP overhead
per request:

Micro (per-call, single goroutine):
  RegisterRequest/NoRates              2 ns/op    0 allocs
  RegisterRequest/HighDefaultRate    306 ns/op    3 allocs
  UnaryInterceptor/NoLimits           19 ns/op    0 allocs
  UnaryInterceptor/HighDefaultRate   322 ns/op    3 allocs

Macro (real TCP server, concurrent clients):
  HTTPServer/NoLimits               9510 ns/op   54 allocs
  HTTPServer/HighDefaultRate        9600 ns/op   57 allocs

This is a follow-on from PR gravitational/teleport#63720,
which added connection limiter support to the app service. While working
on that PR, I kept tripping over the fact that NewRateLimiter always
creates a TokenLimiter with an FnCache and background cleanup goroutine,
even when no rates are configured - which is the common case for most
services. That motivated this cleanup.


I'm on the fence about whether this PR is worth merging. The performance
gain is not meaningful in practice, and any code change to the limiter
carries risk of introducing subtle issues. Looking for guidance on whether
to close this or proceed.

This comment was marked as outdated.

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@juliaogris juliaogris force-pushed the julia/limiter branch 3 times, most recently from cceec54 to 9310bb2 Compare February 20, 2026 04:19
@juliaogris juliaogris requested a review from Copilot March 12, 2026 07:59
@juliaogris juliaogris added the no-test-plan Bypasses the test plan validation bot label Mar 12, 2026

This comment was marked as resolved.

@juliaogris
Copy link
Contributor Author

@codex PTAL

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@juliaogris juliaogris force-pushed the julia/limiter branch 2 times, most recently from 44903da to b0f9512 Compare March 12, 2026 23:47
Add micro-benchmarks comparing the cost of each limiter path when no
limits are configured (noop) against a high default rate (equivalent
to the old 100M req/s fake rate). Cover all call sites:
RegisterRequest, HTTP middleware, RegisterRequestAndConnection, and
both gRPC interceptors (unary and stream).

Also add a macro-benchmark (BenchmarkHTTPServer) that measures
end-to-end HTTP throughput through a real TCP listener with concurrent
clients to show how the limiter's overhead compares to network I/O.

Before the short-circuit fix, NoRates and HighDefaultRate produce
identical results because `NewRateLimiter` injects the fake default:

    BenchmarkRegisterRequest/NoRates              298 ns/op   64 B/op   3 allocs/op
    BenchmarkRegisterRequest/HighDefaultRate       299 ns/op   64 B/op   3 allocs/op
    BenchmarkHTTPMiddleware/NoLimits               405 ns/op  264 B/op   7 allocs/op
    BenchmarkHTTPMiddleware/HighDefaultRate         406 ns/op  264 B/op   7 allocs/op
    BenchmarkRegisterRequestAndConnection/NoLimits  320 ns/op   96 B/op   4 allocs/op
    BenchmarkRegisterRequestAndConnection/HighDefaultRate 321 ns/op 96 B/op 4 allocs/op
    BenchmarkUnaryInterceptor/NoLimits             320 ns/op   64 B/op   3 allocs/op
    BenchmarkUnaryInterceptor/HighDefaultRate       321 ns/op   64 B/op   3 allocs/op
    BenchmarkStreamInterceptor/NoLimits            338 ns/op   96 B/op   4 allocs/op
    BenchmarkStreamInterceptor/HighDefaultRate      339 ns/op   96 B/op   4 allocs/op
    BenchmarkHTTPServer/NoLimits                  9770 ns/op 4752 B/op  57 allocs/op
    BenchmarkHTTPServer/HighDefaultRate            9700 ns/op 4752 B/op  57 allocs/op

Apple M4 Pro, go1.26.1 darwin/arm64.
Every Teleport service creates a Limiter unconditionally, even when
no `connection_limits` are set. Previously, `NewRateLimiter` injected
a fake default rate of 100M req/s to satisfy a non-empty-rates
invariant in `TokenLimiter`. Every request still paid the full cost:
extract client IP, acquire mutex, FnCache lookup, and token bucket
consume - all for a rate that never rejects.

Remove the fake default rate and add early returns throughout the
limiter stack: `RateLimiter.RegisterRequest` returns immediately when
the effective rate set is empty, `TokenLimiter.ServeHTTP` skips IP
extraction and rate consumption, and `ConnectionsLimiter` skips
tracking when `maxConnections` is zero.

Micro-benchmarks on Apple M4 Pro show the noop path is 5-160x faster
in isolation (NoLimits = noop after fix, HighDefaultRate = old
behavior with fake 100M rate):

    BenchmarkRegisterRequest/NoRates              1.85 ns/op    0 B/op   0 allocs/op
    BenchmarkRegisterRequest/HighDefaultRate       306 ns/op   64 B/op   3 allocs/op
    BenchmarkHTTPMiddleware/NoLimits              82.0 ns/op  208 B/op   4 allocs/op
    BenchmarkHTTPMiddleware/HighDefaultRate        405 ns/op  264 B/op   7 allocs/op
    BenchmarkRegisterRequestAndConnection/NoLimits 18.8 ns/op  32 B/op   1 allocs/op
    BenchmarkRegisterRequestAndConnection/HighDefaultRate 323 ns/op 96 B/op 4 allocs/op
    BenchmarkUnaryInterceptor/NoLimits            18.5 ns/op    0 B/op   0 allocs/op
    BenchmarkUnaryInterceptor/HighDefaultRate      322 ns/op   64 B/op   3 allocs/op
    BenchmarkStreamInterceptor/NoLimits           33.2 ns/op   32 B/op   1 allocs/op
    BenchmarkStreamInterceptor/HighDefaultRate     337 ns/op   96 B/op   4 allocs/op

However, a macro-benchmark through a real TCP HTTP server shows no
measurable difference in end-to-end throughput. The limiter's ~300ns
is noise against the ~9500ns of TCP/HTTP overhead per request:

    BenchmarkHTTPServer/NoLimits                  9510 ns/op 4681 B/op  54 allocs/op
    BenchmarkHTTPServer/HighDefaultRate            9600 ns/op 4752 B/op  57 allocs/op

This change is primarily about code clarity - remove the fake default
rate workaround and make the limiter honestly do nothing when no
limits are configured - rather than a meaningful throughput
improvement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-test-plan Bypasses the test plan validation bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants