Skip to content

feat: add rate limiting interceptor with pluggable limiter#39

Merged
ankurs merged 3 commits intomainfrom
feat/rate-limit-interceptor
Apr 9, 2026
Merged

feat: add rate limiting interceptor with pluggable limiter#39
ankurs merged 3 commits intomainfrom
feat/rate-limit-interceptor

Conversation

@ankurs
Copy link
Copy Markdown
Member

@ankurs ankurs commented Apr 9, 2026

Summary

  • Built-in token bucket rate limiter using go-grpc-middleware v2 ratelimit interceptor (already a dependency) + golang.org/x/time/rate
  • Disabled by default (rate.Inf) — no breaking changes, opt-in only
  • Per-pod in-memory limit — with N pods, effective cluster-wide limit is N × configured rate. For distributed rate limiting (per-tenant, Redis-backed), use SetRateLimiter() with a custom implementation
  • Positioned after timeout, before logging in the default chain (reject early)
  • Both unary and stream interceptors

Config API:

  • SetDefaultRateLimit(rps, burst) — configure token bucket
  • SetRateLimiter(limiter) — plug in custom implementation
  • SetDisableRateLimit(true) — disable entirely

Test plan

  • TestRateLimitInterceptor_DefaultInf — default returns nil limiter
  • TestRateLimitInterceptor_Allowed — 1000 rps, request passes
  • TestRateLimitInterceptor_Exceeded — 1 rps burst 1, second request gets ResourceExhausted
  • TestRateLimitInterceptor_CustomLimiter — custom always-reject limiter works
  • TestRateLimitInterceptor_Disabled — disabled via config, no rate limiting
  • make test and make lint clean

Summary by CodeRabbit

  • New Features

    • Server-side gRPC rate limiting with configurable requests-per-second and burst behavior; excess requests are rejected with appropriate error responses.
    • Public controls to enable/disable rate limiting and to supply or tune custom limiter implementations.
  • Tests

    • Added coverage for default and custom rate-limit behaviors, disabling rate limits, and expected error codes.
  • Chores

    • Updated project to include new timing/rate utilities required for rate-limiting support.

Built-in token bucket rate limiter using go-grpc-middleware v2 ratelimit
and golang.org/x/time/rate. Disabled by default (rate.Inf).

Features:
- SetDefaultRateLimit(rps, burst) for per-pod token bucket
- SetRateLimiter() for custom implementations (Redis, per-tenant, etc.)
- SetDisableRateLimit() to disable entirely
- Both unary and stream interceptors
- Positioned after timeout, before logging in the chain

This is a per-pod in-memory limit. With N pods, effective cluster-wide
limit is N × configured rate.
Copilot AI review requested due to automatic review settings April 9, 2026 10:30
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 326770f1-0a3f-48d7-af98-f5cad0f28a16

📥 Commits

Reviewing files that changed from the base of the PR and between b2928ad and 8d8b581.

📒 Files selected for processing (1)
  • README.md

📝 Walkthrough

Walkthrough

Adds server-side gRPC rate limiting: new golang.org/x/time v0.15.0 module requirement, integration of a rate-limit middleware with a token-bucket adapter, exported rate-limit configuration APIs, lazy limiter initialization, and tests covering limiter behaviors.

Changes

Cohort / File(s) Summary
Module Dependency
go.mod
Added golang.org/x/time v0.15.0 to require.
Rate Limiting Implementation
interceptors.go
Integrated gRPC rate-limit middleware; introduced package-level rate-limit state and defaults, tokenBucketLimiter adapter using rate.Limiter, lazy init via sync.Once, conditional insertion of unary/stream ratelimit interceptors, and exported APIs: SetDisableRateLimit(), SetRateLimiter(), SetDefaultRateLimit().
Tests
interceptors_test.go
Extended tests and resetGlobals() to cover rate-limit state: default (infinite) limiter, configured rate allowing requests, rejection with codes.ResourceExhausted when exceeded, custom rejecting limiter, and disabling rate limiting.
Docs / README
README.md
Updated generated API index and anchors to include new interceptor/configuration functions (rate-limiting and debug-log related entries).

Sequence Diagram

sequenceDiagram
    autonumber
    actor Client
    participant Server as gRPC Server
    participant Interceptor as RateLimit<br/>Interceptor
    participant Limiter as TokenBucket<br/>Limiter
    participant Handler as Request<br/>Handler

    Client->>Server: gRPC request
    Server->>Interceptor: invoke interceptor chain
    Interceptor->>Limiter: Allow()
    alt Tokens available
        Limiter-->>Interceptor: allowed
        Interceptor->>Handler: proceed to handler
        Handler-->>Client: response
    else Rate limit exceeded
        Limiter-->>Interceptor: error "rate limit exceeded"
        Interceptor-->>Client: ResourceExhausted error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I count the hops and tally the beam,
Carrots of tokens in a calm little stream,
When bursts are gentle and limits align,
Requests bounce through one at a time,
Hooray — steady hops, no frantic whine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding a rate limiting interceptor with support for custom/pluggable limiters.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rate-limit-interceptor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in gRPC rate limiting interceptor to the ColdBrew default interceptor chains, with support for either a built-in token-bucket limiter or a user-provided limiter implementation.

Changes:

  • Introduce rate-limit configuration globals and initialization-time setters (SetDefaultRateLimit, SetRateLimiter, SetDisableRateLimit).
  • Insert unary + stream rate limiting into the default server interceptor chains (unary placed after timeout, before logging).
  • Add unit tests for unary rate limiting behavior and update module deps for golang.org/x/time/rate.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.

File Description
interceptors.go Adds limiter config API, token-bucket limiter, and inserts rate limit interceptors into default unary/stream chains.
interceptors_test.go Adds unary rate limit tests and resets new globals in test setup.
go.mod Adds direct dependency on golang.org/x/time.
go.sum Adds checksums for golang.org/x/time.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread interceptors.go
Comment thread interceptors.go
Comment thread interceptors.go
Comment thread interceptors_test.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
interceptors_test.go (1)

1320-1437: Add a stream-path regression test.

These cases validate the unary limiter behavior, but the PR also wires rate limiting into DefaultStreamInterceptors(). A small stream allow/exceed test would cover the new production branch and catch wiring/order regressions that this suite currently misses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interceptors_test.go` around lines 1320 - 1437, Add a stream-path regression
test that mirrors the unary tests: create
TestRateLimitInterceptor_StreamAllowedAndExceeded which calls
SetDefaultRateLimit(...) and obtains limiter via getRateLimiter(), then builds a
grpc.StreamServerInfo{FullMethod: "/test/RateLimitStream"} and a minimal
fakeServerStream implementing grpc.ServerStream (Context, RecvMsg, SendMsg) to
use as the stream argument; get the interceptor via
ratelimit_middleware.StreamServerInterceptor(limiter) and invoke it once
expecting success and a second time expecting a ResourceExhausted error. Also
add a small TestRateLimitInterceptor_StreamDisabled that calls
SetDisableRateLimit(true) and runs DefaultStreamInterceptors() over the
fakeServerStream to assert no ResourceExhausted is returned. Ensure tests
reference getRateLimiter, SetDefaultRateLimit, SetDisableRateLimit,
ratelimit_middleware.StreamServerInterceptor, and DefaultStreamInterceptors so
the stream branch is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interceptors.go`:
- Around line 900-903: SetDefaultRateLimit currently allows a finite rps with
burst < 1 which results in a deny-all limiter; add input validation in
SetDefaultRateLimit to detect finite rps (use math.IsInf/math.IsNaN on the rps
value) and if rps is finite and burst < 1, clamp burst to 1 (or set
defaultRateBurst = 1) before assigning defaultRateLimit = rate.Limit(rps) and
defaultRateBurst; reference SetDefaultRateLimit, defaultRateLimit, and
defaultRateBurst when making the change so the limiter is never configured with
a zero burst for finite rates.

---

Nitpick comments:
In `@interceptors_test.go`:
- Around line 1320-1437: Add a stream-path regression test that mirrors the
unary tests: create TestRateLimitInterceptor_StreamAllowedAndExceeded which
calls SetDefaultRateLimit(...) and obtains limiter via getRateLimiter(), then
builds a grpc.StreamServerInfo{FullMethod: "/test/RateLimitStream"} and a
minimal fakeServerStream implementing grpc.ServerStream (Context, RecvMsg,
SendMsg) to use as the stream argument; get the interceptor via
ratelimit_middleware.StreamServerInterceptor(limiter) and invoke it once
expecting success and a second time expecting a ResourceExhausted error. Also
add a small TestRateLimitInterceptor_StreamDisabled that calls
SetDisableRateLimit(true) and runs DefaultStreamInterceptors() over the
fakeServerStream to assert no ResourceExhausted is returned. Ensure tests
reference getRateLimiter, SetDefaultRateLimit, SetDisableRateLimit,
ratelimit_middleware.StreamServerInterceptor, and DefaultStreamInterceptors so
the stream branch is exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59ba9693-3b28-416b-bc79-0b75a8123c29

📥 Commits

Reviewing files that changed from the base of the PR and between 93d68bc and 163b886.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • go.mod
  • interceptors.go
  • interceptors_test.go

Comment thread interceptors.go
ankurs added 2 commits April 9, 2026 19:27
Address review:
- Fix gofumpt import ordering (golang.org/x after github.com)
- Bump golang.org/x/time v0.3.0 → v0.15.0
- Clamp burst to min 1 in SetDefaultRateLimit (burst=0 rejects all)
- Remove dead var _ = rate.Inf from tests
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread interceptors.go
@ankurs ankurs merged commit 30ca650 into main Apr 9, 2026
11 checks passed
@ankurs ankurs deleted the feat/rate-limit-interceptor branch April 9, 2026 11:46
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.

2 participants