Skip to content

fix: use ratelimit name in bucket#4233

Merged
Flo4604 merged 3 commits intomainfrom
eng-2179-ensure-v2-rate-limits-are-isolated-by-name
Nov 5, 2025
Merged

fix: use ratelimit name in bucket#4233
Flo4604 merged 3 commits intomainfrom
eng-2179-ensure-v2-rate-limits-are-isolated-by-name

Conversation

@Flo4604
Copy link
Member

@Flo4604 Flo4604 commented Nov 4, 2025

What does this PR do?

Fixes #4201

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

Run tests.

When a identity has the same limit applied twice to it that maps to a unique bucket (same identifier (identity-id) + duration + limit) then we would re-use the same bucket even tho thats not what should happen.

So now we just add the ratelimit name into the bucket key in order to make them unique to mirror the v1.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Ran make fmt on /go directory
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

@linear
Copy link

linear bot commented Nov 4, 2025

@changeset-bot
Copy link

changeset-bot bot commented Nov 4, 2025

⚠️ No Changeset found

Latest commit: a827f63

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Nov 4, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Nov 5, 2025 1:47pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
engineering Ignored Ignored Preview Nov 5, 2025 1:47pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 4, 2025

📝 Walkthrough

Walkthrough

Adds a Name field to ratelimit requests and threads it from handler → validation → ratelimit service; includes Name in bucketKey and bucket identity so limits with same config but different names are isolated. Adds a test asserting per-name isolation (duplicated block present).

Changes

Cohort / File(s) Summary
Tests
go/apps/api/routes/v2_keys_verify_key/ratelimit_response_test.go
Adds a test asserting that two rate limits with identical duration/limit but different names are tracked independently; the same test block appears duplicated.
Public request model
go/internal/services/ratelimit/interface.go
Adds Name string to RatelimitRequest.
Handler & validation
go/apps/api/routes/v2_ratelimit_limit/handler.go, go/internal/services/keys/validation.go
Handler now sets Name (from namespace.ID) on requests; validation propagates per-config Name into constructed ratelimit requests.
Ratelimit core (keys & buckets)
go/internal/services/ratelimit/bucket.go, go/internal/services/ratelimit/replay.go, go/internal/services/ratelimit/service.go
Introduces name on bucket/bucketKey; includes Name when building keys and initializing buckets; adds validation that RatelimitRequest.Name is non-empty; updates key string serialization to include Name.

Sequence Diagram

sequenceDiagram
    participant Handler as Handler\n(v2_ratelimit_limit)
    participant Validation as Validation\n(keys/validation.go)
    participant Service as Ratelimit Service\n(service.go)
    participant Bucket as Bucket Store\n(bucket.go)

    Handler->>Validation: construct ratelimit request\n(Name = namespace.ID, Identifier)
    Validation->>Service: send Ratelimit(s) with Name field
    Service->>Service: validate req.Name != ""\nconstruct bucketKey {Name, Identifier, Limit, Duration}
    Service->>Bucket: getOrCreateBucket(key)
    Bucket->>Bucket: create/lookup bucket using Name as part of key
    Bucket->>Service: return counters/result
    Service->>Validation: map results back to configs
    Validation->>Handler: return aggregated response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review points: verify the duplicated test block should be removed or consolidated; confirm all call sites populate RatelimitRequest.Name; check any consumers/serializers of bucketKey.toString() for ordering assumptions.

Possibly related PRs

Suggested labels

Bug, Needs Approval

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: use ratelimit name in bucket' accurately summarizes the main change: incorporating the rate limit name into the bucket key to ensure isolation.
Description check ✅ Passed The PR description includes the issue reference (#4201), change type, problem statement, solution approach, and test instructions, covering all essential template sections.
Linked Issues check ✅ Passed The PR successfully implements the objective from #4201 by adding the Name field to RatelimitRequest and threading it through the bucket key in all relevant service paths.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing #4201: adding Name field to RatelimitRequest, propagating it through validation/handler logic, and including it in bucket key construction.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch eng-2179-ensure-v2-rate-limits-are-isolated-by-name

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d9624c and a827f63.

📒 Files selected for processing (2)
  • go/internal/services/ratelimit/interface.go (1 hunks)
  • go/internal/services/ratelimit/service.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go/internal/services/ratelimit/interface.go
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
Learnt from: imeyer
Repo: unkeyed/unkey PR: 3755
File: .github/workflows/runbook-freshness-check.yaml:157-173
Timestamp: 2025-08-08T14:59:52.283Z
Learning: Repo unkeyed/unkey: When a CI/workflow fix is deferred, imeyer prefers a thorough GitHub issue be opened with sections (Summary, Impact, Where, Repro, Observed vs Expected, Acceptance Criteria, Validation Plan, Out of Scope, References) and assigned to imeyer, including backlinks to the originating PR and comment.
📚 Learning: 2025-08-29T13:48:43.353Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 3884
File: go/apps/api/routes/v2_ratelimit_delete_override/handler.go:218-228
Timestamp: 2025-08-29T13:48:43.353Z
Learning: In the unkeyed/unkey codebase, when working with ratelimit namespace caching, req.Namespace parameter is either the namespace ID or the namespace name, so cache invalidation only needs to remove cache keys for namespace.ID and namespace.Name - no need to also remove req.Namespace as a separate key.

Applied to files:

  • go/internal/services/ratelimit/service.go
📚 Learning: 2025-08-21T15:54:45.198Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 3825
File: go/internal/services/usagelimiter/limit.go:38-0
Timestamp: 2025-08-21T15:54:45.198Z
Learning: In go/internal/services/usagelimiter/limit.go, the UpdateKeyCreditsDecrement operation cannot be safely wrapped with db.WithRetry due to the lack of idempotency mechanisms in the current tech stack. Retrying this non-idempotent write operation risks double-charging users if the first attempt commits but the client sees a transient error.

Applied to files:

  • go/internal/services/ratelimit/service.go
📚 Learning: 2025-08-19T09:42:40.919Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 3800
File: go/internal/services/keys/validation.go:45-52
Timestamp: 2025-08-19T09:42:40.919Z
Learning: In go/internal/services/keys/validation.go, keys with unlimited usage (RemainingRequests.Valid = false) have an early return that bypasses the usage limiter entirely. The usage limiter is only called for keys with finite remaining request counts.

Applied to files:

  • go/internal/services/ratelimit/service.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test API / API Test Local
🔇 Additional comments (2)
go/internal/services/ratelimit/service.go (2)

171-188: Nice guardrail for name-specific buckets.

Validating req.Name and threading it into the bucket key closes the reuse hole when two limits share identifier/duration but differ only by name. This keeps v2 behavior aligned with the v1 fix and guarantees isolation as intended.


276-289: Single-limit path also gets the proper bucket uniqueness.

Mirroring the validation and key construction in Ratelimit ensures the single-request path can’t fall back to the shared bucket either. Thanks for keeping both call sites in sync.


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.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 4, 2025

Thank you for following the naming conventions for pull request titles! 🙏

Copy link
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go/internal/services/ratelimit/service.go (1)

171-177: Add Name validation to RatelimitMany for consistency.

The Ratelimit method validates that req.Name is not empty (line 277), but RatelimitMany does not include this validation. Since both methods construct bucket keys using req.Name, this inconsistency could allow empty names in RatelimitMany, potentially causing incorrect bucket key generation.

Apply this diff to add the missing validation:

 		err := assert.All(
+			assert.NotEmpty(reqs[i].Name, "ratelimit name must not be empty"),
 			assert.NotEmpty(reqs[i].Identifier, "ratelimit identifier must not be empty"),
 			assert.Greater(reqs[i].Limit, 0, "ratelimit limit must be greater than zero"),
 			assert.GreaterOrEqual(reqs[i].Cost, 0, "ratelimit cost must not be negative"),
 			assert.GreaterOrEqual(reqs[i].Duration.Milliseconds(), 1000, "ratelimit duration must be at least 1s"),
 			assert.False(reqs[i].Time.IsZero(), "request time must not be zero"),
 		)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9d581d and 9d9624c.

📒 Files selected for processing (7)
  • go/apps/api/routes/v2_keys_verify_key/ratelimit_response_test.go (1 hunks)
  • go/apps/api/routes/v2_ratelimit_limit/handler.go (1 hunks)
  • go/internal/services/keys/validation.go (1 hunks)
  • go/internal/services/ratelimit/bucket.go (5 hunks)
  • go/internal/services/ratelimit/interface.go (1 hunks)
  • go/internal/services/ratelimit/replay.go (1 hunks)
  • go/internal/services/ratelimit/service.go (3 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2025-08-29T13:48:43.353Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 3884
File: go/apps/api/routes/v2_ratelimit_delete_override/handler.go:218-228
Timestamp: 2025-08-29T13:48:43.353Z
Learning: In the unkeyed/unkey codebase, when working with ratelimit namespace caching, req.Namespace parameter is either the namespace ID or the namespace name, so cache invalidation only needs to remove cache keys for namespace.ID and namespace.Name - no need to also remove req.Namespace as a separate key.

Applied to files:

  • go/internal/services/ratelimit/replay.go
  • go/apps/api/routes/v2_ratelimit_limit/handler.go
  • go/internal/services/keys/validation.go
  • go/internal/services/ratelimit/service.go
📚 Learning: 2025-08-21T15:54:45.198Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 3825
File: go/internal/services/usagelimiter/limit.go:38-0
Timestamp: 2025-08-21T15:54:45.198Z
Learning: In go/internal/services/usagelimiter/limit.go, the UpdateKeyCreditsDecrement operation cannot be safely wrapped with db.WithRetry due to the lack of idempotency mechanisms in the current tech stack. Retrying this non-idempotent write operation risks double-charging users if the first attempt commits but the client sees a transient error.

Applied to files:

  • go/internal/services/ratelimit/replay.go
  • go/internal/services/keys/validation.go
  • go/internal/services/ratelimit/service.go
📚 Learning: 2025-10-21T09:45:47.560Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 4107
File: go/pkg/clickhouse/key_verifications_test.go:20-20
Timestamp: 2025-10-21T09:45:47.560Z
Learning: In ClickHouse tests (e.g., go/pkg/clickhouse/key_verifications_test.go), parallel execution with t.Parallel() is safe when tests use workspaceID-based isolation. Each test generates a unique workspaceID (via uid.New(uid.WorkspacePrefix)), and all database operations are scoped to that workspaceID, providing logical isolation even when tests share a single ClickHouse instance.

Applied to files:

  • go/apps/api/routes/v2_keys_verify_key/ratelimit_response_test.go
📚 Learning: 2025-08-19T09:42:40.919Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 3800
File: go/internal/services/keys/validation.go:45-52
Timestamp: 2025-08-19T09:42:40.919Z
Learning: In go/internal/services/keys/validation.go, keys with unlimited usage (RemainingRequests.Valid = false) have an early return that bypasses the usage limiter entirely. The usage limiter is only called for keys with finite remaining request counts.

Applied to files:

  • go/internal/services/keys/validation.go
📚 Learning: 2024-10-20T07:05:55.471Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 2294
File: apps/api/src/pkg/keys/service.ts:268-271
Timestamp: 2024-10-20T07:05:55.471Z
Learning: In `apps/api/src/pkg/keys/service.ts`, `ratelimitAsync` is a table relation, not a column selection. When querying, ensure that table relations are included appropriately, not as columns.

Applied to files:

  • go/internal/services/keys/validation.go
🧬 Code graph analysis (1)
go/apps/api/routes/v2_keys_verify_key/ratelimit_response_test.go (3)
go/pkg/testutil/seed/seed.go (2)
  • CreateIdentityRequest (368-373)
  • CreateRatelimitRequest (310-318)
go/apps/api/openapi/gen.go (1)
  • VerifyKeyRatelimitData (2047-2072)
go/pkg/testutil/http.go (1)
  • CallRoute (385-419)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (10)
go/internal/services/ratelimit/replay.go (1)

62-67: LGTM! Name field correctly propagated to bucket key.

The addition of name: req.Name ensures that replay requests use the same expanded bucket key structure as the main rate limiting paths, maintaining consistency across the cluster.

go/apps/api/routes/v2_keys_verify_key/ratelimit_response_test.go (1)

253-335: LGTM! Comprehensive test validates name-based isolation.

This test correctly verifies that rate limits with identical configurations but different names maintain independent counters. The test confirms:

  1. Both api_requests and data_access limits are auto-applied and checked on each request
  2. Each maintains its own counter (validates the bug fix)
  3. Both decrement independently through 5 requests
  4. Both exceed on the 6th request as expected

The test effectively validates the core objective of this PR.

go/internal/services/keys/validation.go (1)

188-197: LGTM! Name field properly propagated in validation layer.

The addition of Name: config.Name ensures that rate limit requests constructed during key validation include the rate limit name, enabling downstream services to create properly isolated buckets.

go/internal/services/ratelimit/service.go (2)

186-192: LGTM! Bucket key construction includes Name.

The bucket key now correctly includes req.Name as the first component, ensuring rate limits are properly isolated by name.


275-288: LGTM! Name validation and bucket key construction are correct.

The validation ensures req.Name is not empty, and the bucket key construction properly includes it for isolation.

go/internal/services/ratelimit/bucket.go (4)

34-35: LGTM! Name field added to bucket struct.

The addition of the name field to the bucket enables proper isolation of rate limits by name.


57-64: LGTM! Bucket key includes name.

The key() method correctly includes the bucket's name when constructing the bucketKey.


83-98: LGTM! BucketKey structure and serialization updated correctly.

The bucketKey struct now includes the name field, and the toString() method properly includes it as the first component in the key string, ensuring unique identification.


120-128: LGTM! Bucket initialization includes name.

The getOrCreateBucket function correctly initializes the bucket's name field from the provided key.

go/apps/api/routes/v2_ratelimit_limit/handler.go (1)

259-266: LGTM! Correct separation of Name and Identifier.

The changes properly distinguish between:

  • Name: Set to namespace.ID to identify the rate limit configuration
  • Identifier: Set to req.Identifier to identify the entity being rate limited

This ensures that rate limits are correctly isolated by namespace while still tracking per-identifier limits.

Copy link
Member Author

Flo4604 commented Nov 4, 2025

good catch

@chronark
Copy link
Collaborator

chronark commented Nov 5, 2025

@ogzhanolguncu

@vercel vercel bot temporarily deployed to Preview – dashboard November 5, 2025 13:47 Inactive
@graphite-app
Copy link

graphite-app bot commented Nov 5, 2025

Video gif. A toddler grins wide, raising their hand and giving an exuberant thumbs up at us. Their nose krinkles under their big smile.  (Added via Giphy)

@graphite-app
Copy link

graphite-app bot commented Nov 5, 2025

Graphite Automations

"Post a GIF when PR approved" took an action on this PR • (11/05/25)

1 gif was posted to this PR based on Andreas Thomas's automation.

@Flo4604 Flo4604 added this pull request to the merge queue Nov 5, 2025
Merged via the queue into main with commit 2e8de26 Nov 5, 2025
28 of 31 checks passed
@Flo4604 Flo4604 deleted the eng-2179-ensure-v2-rate-limits-are-isolated-by-name branch November 5, 2025 19:23
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.

Ensure v2 rate limits are isolated by name

3 participants