Skip to content

Simple Rate Limiting - #4

Merged
alexeichhorn merged 5 commits into
cloudflare-workerfrom
feature/simple-rate-limiting
Feb 9, 2026
Merged

alexeichhorn merged 5 commits into
cloudflare-workerfrom
feature/simple-rate-limiting

Conversation

@alexeichhorn

@alexeichhorn alexeichhorn commented Feb 9, 2026 •

Copy link
Copy Markdown
Owner

Note

Medium Risk
Introduces a new Durable Object backed by SQLite and gates WebSocket upgrades on it; incorrect limits, binding/migration config, or DO failures could block legitimate traffic or return 429/503 unexpectedly.

Overview
Adds per-appID rate limiting for WebSocket connections (/v1 upgrades) using a new Durable Object (AppRateLimiter) with SQLite-backed anchored day/week windows and configurable limits.

Worker requests now normalize X-AppID-v1, call the DO to admit/deny, and return 429 with Retry-After + rate limit headers when exceeded (or 503 if the limiter errors). wrangler.toml/worker-configuration.d.ts are updated with the DO binding, migration, and RATE_LIMIT_DAILY_REQUESTS/RATE_LIMIT_WEEKLY_REQUESTS vars; README documents the new behavior, and package.json adds a typecheck script.

Written by Cursor Bugbot for commit 6a7ed36. This will update automatically on new commits. Configure here.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Feb 9, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
youtubekit-server 6a7ed36 Feb 09 2026, 07:31 PM

@alexeichhorn
alexeichhorn marked this pull request as ready for review February 9, 2026 19:38
@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces a Cloudflare Durable Objects-based rate limiter supporting daily and weekly quota windows with automatic window rolling and retry-after calculations. Adds comprehensive documentation for Durable Objects best practices, Wrangler CLI usage, and testing patterns. Updates type definitions to include workflow retention semantics and expanded telemetry attributes.

Changes

Cohort / File(s) Summary
Documentation & Skills
.agents/skills/durable-objects/SKILL.md, .agents/skills/durable-objects/references/rules.md, .agents/skills/durable-objects/references/testing.md, .agents/skills/durable-objects/references/workers.md, .agents/skills/wrangler/SKILL.md
Comprehensive guides covering Durable Objects design patterns, testing strategies with vitest-pool-workers, Workers integration best practices, and detailed Wrangler CLI reference with configuration and binding examples.
Rate Limiter Implementation
src/durable-objects/app-rate-limiter.ts
New Durable Object class implementing per-app-ID rate limiting with daily/weekly quota windows. Tracks window start times and counts; rolls expired windows; computes retry-after based on reset times; validates against configurable limits (env vars).
Integration & HTTP Handler
src/index.ts
Exports AppRateLimiter; adds rate-limit check before WebSocket upgrade using derived DO object ID from App ID header. Includes App ID normalization, rate-limiter queries, and 429/503 error responses with rate-limit headers.
Configuration & Type Definitions
worker-configuration.d.ts, wrangler.toml
Extends Cloudflare.Env with rate-limit vars and APP_RATE_LIMITER binding. Adds DO migration for AppRateLimiter schema. Expands TailStream telemetry types (Attribute, Attributes) and Workflow retention semantics (WorkflowRetentionDuration, successRetention/errorRetention).
Build & Documentation
package.json, README.md
Adds "typecheck" npm script. Reformats README prerequisites and introduces new "Rate Limiting" section documenting window behavior and configurable daily/weekly request limits.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Worker
    participant AppRateLimiter as App Rate Limiter<br/>(Durable Object)
    participant Storage

    Client->>Worker: WebSocket upgrade request<br/>(with App-ID header)
    Worker->>Worker: Normalize & extract App ID
    Worker->>AppRateLimiter: admit(cost)
    AppRateLimiter->>Storage: Fetch limiter_state<br/>(for this App ID)
    AppRateLimiter->>AppRateLimiter: Roll expired windows<br/>(daily & weekly)
    AppRateLimiter->>AppRateLimiter: Check daily limit<br/>Check weekly limit
    alt Quota Exceeded
        AppRateLimiter->>AppRateLimiter: Calculate retryAfterSeconds<br/>(from window resets)
        AppRateLimiter-->>Worker: RateLimitDecision<br/>(allowed=false)
        Worker-->>Client: 429 Too Many Requests<br/>(with rate-limit headers)
    else Within Limits
        AppRateLimiter->>Storage: Upsert limiter_state<br/>(increment counts)
        AppRateLimiter-->>Worker: RateLimitDecision<br/>(allowed=true)
        Worker->>Worker: Proceed with<br/>WebSocket upgrade
        Worker-->>Client: 101 Switching Protocols
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title 'Simple Rate Limiting' directly matches the main objective of the PR, which introduces per-appID rate limiting for WebSocket connections.
Description check ✅ Passed The description comprehensively explains the rate-limiting feature, including the Durable Object implementation, configuration changes, and risk assessment, all of which align with the changeset.

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

✨ 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 feature/simple-rate-limiting

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In @.agents/skills/durable-objects/references/testing.md:
- Around line 9-11: Update the vitest version recommendation in the testing doc
so it matches the project's package.json (change `vitest@~3.2.0` to
`vitest@~3.0.7`) or replace the pinned version with a generic reference like
`vitest` to avoid mismatch; ensure the command line in the doc still includes
`@cloudflare/vitest-pool-workers` and that the example uses the same versioning
approach as the project's package.json.

In @.agents/skills/durable-objects/references/workers.md:
- Around line 344-345: Remove the incorrect example command `wrangler d1 execute
DB --command "SELECT * FROM _cf_DO"` and replace it with accurate guidance:
Durable Objects are not listed via a D1 SQL command — tell readers to view
Durable Objects in the Cloudflare dashboard or inspect bindings in wrangler.toml
(or point to the Cloudflare API for programmatic queries) and provide a short
example/reference to the dashboard or API instead of the bogus `_cf_DO` command.

In @.agents/skills/wrangler/SKILL.md:
- Around line 196-201: The fenced code block showing the `.dev.vars` example is
missing a language specifier; update that snippet (the triple-backtick block
containing "API_KEY=local-dev-key" and
"DATABASE_URL=postgres://localhost:5432/dev") to include a language such as ini
(e.g., change ``` to ```ini) so markdownlint MD040 is satisfied and the block is
properly highlighted.

In `@README.md`:
- Line 42: Update the README sentence to make it explicit that rate limiting
applies only to WebSocket upgrade requests on /v1, not all HTTP requests:
mention the header/identifier `X-AppID-v1`, the `/v1` path, and that a Durable
Object with anchored windows enforces limits specifically for WebSocket
connection upgrades; revise the line "Requests are limited per app ID
(`X-AppID-v1`) using a Durable Object with anchored windows:" to something like
"WebSocket upgrade requests to /v1 are rate-limited per app ID (`X-AppID-v1`)
via a Durable Object with anchored windows."

In `@src/durable-objects/app-rate-limiter.ts`:
- Around line 72-87: The deny response computes remainingDaily/remainingWeekly
from nextState.dayCount/weekCount which can show positive remaining even when
the attempted cost caused the denial; update the deny branch to compute
remaining values from policy.dailyLimit - nextDaily and policy.weeklyLimit -
nextWeekly (clamped to >=0) so the response reflects the attempted cost (use
nextDaily, nextWeekly, policy.dailyLimit, policy.weeklyLimit and keep
retryAfterSeconds via calculateRetryAfterSeconds).

In `@src/index.ts`:
- Around line 29-48: The code logs raw App IDs (PII) when reading the header via
APP_ID_HEADER and normalizing with normalizeAppID; change logging in the
websocket upgrade handling and the rate-limit warning in the block around
checkRateLimit(appID, env) to log a redacted or hashed form instead of the raw
appID: compute a deterministic redaction/hash (e.g., SHA256 or stable masking)
of the value returned by normalizeAppID and use that redactedId in console.log
and in the JSON passed to console.warn (including appID -> redactedAppID) so the
rest of the rate-limit fields (limitDaily, remainingDaily, etc.) remain
unchanged. Ensure the redaction function is applied wherever appID is logged or
included in telemetry in this request path.
- Around line 35-37: The code currently uses the client-provided X-AppID-v1
header directly in checkRateLimit(appID, env), allowing clients to bypass
limits; fix by validating/authenticating the appID before rate
limiting—implement an authentication step (e.g., verify JWT/API key/mTLS) in the
request handler (the code that reads X-AppID-v1) and reject requests with
missing/invalid appIDs, or map the authenticated client to a server-side
principal/tenant ID and pass that canonical tenant identifier to checkRateLimit
instead of the raw header; update any Durable Object lookup logic to use the
server-side principal/tenant ID so new client-supplied appIDs cannot spawn
unbounded Durable Objects.
🧹 Nitpick comments (1)
wrangler.toml (1)

17-18: Consider whether default rate limits should differ between dev and production.

Both the root (dev) and production environments use the same limits (5000 daily / 20000 weekly). During local development, a lower limit might be more useful for testing rate-limit behavior, while production might warrant different tuning. This is optional — just calling it out for consideration.

Also applies to: 35-36

Comment thread .agents/skills/durable-objects/references/testing.md
Comment thread .agents/skills/durable-objects/references/workers.md
Comment on lines +196 to +201
Create `.dev.vars` for local development secrets:

```
API_KEY=local-dev-key
DATABASE_URL=postgres://localhost:5432/dev
```

@coderabbitai coderabbitai Bot Feb 9, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language specifier to the fenced code block.

The .dev.vars example block is missing a language identifier, flagged by markdownlint (MD040). Use ```ini or ```bash or ```text to specify the format.

Proposed fix
-```
+```ini
 API_KEY=local-dev-key
 DATABASE_URL=postgres://localhost:5432/dev
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.20.0)</summary>

[warning] 198-198: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @.agents/skills/wrangler/SKILL.md around lines 196 - 201, The fenced code
block showing the .dev.vars example is missing a language specifier; update
that snippet (the triple-backtick block containing "API_KEY=local-dev-key" and
"DATABASE_URL=postgres://localhost:5432/dev") to include a language such as ini
(e.g., change toini) so markdownlint MD040 is satisfied and the block is
properly highlighted.


</details>

<!-- fingerprinting:phantom:poseidon:phoenix -->

<!-- This is an auto-generated reply by CodeRabbit -->

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Just agent skills. No code or user facing docs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@alexeichhorn, understood! Since this is internal agent skills documentation, I'll leave it as-is. Thanks for clarifying!

Comment thread README.md

## Rate Limiting

Requests are limited per app ID (`X-AppID-v1`) using a Durable Object with anchored windows:

@coderabbitai coderabbitai Bot Feb 9, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify that rate limiting applies to WebSocket upgrade requests, not all requests.

The current wording "Requests are limited per app ID" could be interpreted as all HTTP requests being rate-limited. Per the implementation, only WebSocket connection upgrades on /v1 are subject to rate limiting. Consider making this more specific to avoid confusion.

🤖 Prompt for AI Agents
In `@README.md` at line 42, Update the README sentence to make it explicit that
rate limiting applies only to WebSocket upgrade requests on /v1, not all HTTP
requests: mention the header/identifier `X-AppID-v1`, the `/v1` path, and that a
Durable Object with anchored windows enforces limits specifically for WebSocket
connection upgrades; revise the line "Requests are limited per app ID
(`X-AppID-v1`) using a Durable Object with anchored windows:" to something like
"WebSocket upgrade requests to /v1 are rate-limited per app ID (`X-AppID-v1`)
via a Durable Object with anchored windows."

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

There are no other requests....

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@alexeichhorn You're absolutely right! Since this server exclusively handles WebSocket connections for YouTube stream extraction, the current wording is perfectly clear. My suggestion to add extra specificity was unnecessary given that context. Thanks for clarifying! 👍

Comment thread src/durable-objects/app-rate-limiter.ts
Comment thread src/index.ts
Comment on lines 29 to +48
// Log the App ID header for debugging purposes
const appID = request.headers.get('X-AppID-v1') ?? 'unknown';
const appID = normalizeAppID(request.headers.get(APP_ID_HEADER));
console.log(`App ID: ${appID}`);

// Only handle GET /v1?videoID=... as WebSocket upgrades
if (url.pathname === '/v1' && request.headers.get('Upgrade') === 'websocket') {
try {
const decision = await checkRateLimit(appID, env);
if (!decision.allowed) {
console.warn(
'Rate limit rejected request',
JSON.stringify({
appID,
path: url.pathname,
limitDay: decision.limitDaily,
remainingDay: decision.remainingDaily,
limitWeek: decision.limitWeekly,
remainingWeek: decision.remainingWeekly,
retryAfterSeconds: decision.retryAfterSeconds,
})

@coderabbitai coderabbitai Bot Feb 9, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging raw App IDs (privacy/PII exposure).

These logs include the full App ID, which can be a tenant/user identifier. Please redact or hash before logging and use the redacted value in the warning payload.

🔒 Suggested redaction
-      console.log(`App ID: ${appID}`);
+      const appIDForLog = appID === 'unknown' ? appID : `${appID.slice(0, 8)}…`;
+      console.log(`App ID: ${appIDForLog}`);
...
-                     appID,
+                     appID: appIDForLog,
🤖 Prompt for AI Agents
In `@src/index.ts` around lines 29 - 48, The code logs raw App IDs (PII) when
reading the header via APP_ID_HEADER and normalizing with normalizeAppID; change
logging in the websocket upgrade handling and the rate-limit warning in the
block around checkRateLimit(appID, env) to log a redacted or hashed form instead
of the raw appID: compute a deterministic redaction/hash (e.g., SHA256 or stable
masking) of the value returned by normalizeAppID and use that redactedId in
console.log and in the JSON passed to console.warn (including appID ->
redactedAppID) so the rest of the rate-limit fields (limitDaily, remainingDaily,
etc.) remain unchanged. Ensure the redaction function is applied wherever appID
is logged or included in telemetry in this request path.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

No issue. This is not very private

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@alexeichhorn, understood! I'll note that App IDs in this project aren't considered sensitive.

Comment thread src/index.ts
Comment on lines +35 to +37
try {
const decision = await checkRateLimit(appID, env);
if (!decision.allowed) {

@coderabbitai coderabbitai Bot Feb 9, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 "X-AppID-v1|APP_ID_HEADER|normalizeAppID|appID" -S

Repository: alexeichhorn/YouTubeKit-Server

Length of output: 2545


🏁 Script executed:

#!/bin/bash
rg -n -C3 "authorization|auth|jwt|api key|apikey|client id|tenant" -S

Repository: alexeichhorn/YouTubeKit-Server

Length of output: 7061


🏁 Script executed:

#!/bin/bash
# Get context around the specific lines mentioned (35-37 in src/index.ts)
head -60 src/index.ts | tail -35

Repository: alexeichhorn/YouTubeKit-Server

Length of output: 1553


🏁 Script executed:

#!/bin/bash
# Find where appID is extracted/set
rg -n "appID\s*=" -A 2 -B 2

Repository: alexeichhorn/YouTubeKit-Server

Length of output: 320


App ID is client-controlled with no authentication—clients can bypass rate limits and create unbounded Durable Objects.

The X-AppID-v1 header is extracted and used directly for rate limiting without validation or authentication. Clients can freely rotate appIDs to obtain fresh rate limit quotas, and each unique appID creates a separate Durable Object instance, risking resource exhaustion. Implement one of:

  • Require authenticated client credentials (JWT, API key, mTLS)
  • Map client requests to server-side principals/tenants before rate limiting
  • Reject requests with missing or invalid appIDs
🤖 Prompt for AI Agents
In `@src/index.ts` around lines 35 - 37, The code currently uses the
client-provided X-AppID-v1 header directly in checkRateLimit(appID, env),
allowing clients to bypass limits; fix by validating/authenticating the appID
before rate limiting—implement an authentication step (e.g., verify JWT/API
key/mTLS) in the request handler (the code that reads X-AppID-v1) and reject
requests with missing/invalid appIDs, or map the authenticated client to a
server-side principal/tenant ID and pass that canonical tenant identifier to
checkRateLimit instead of the raw header; update any Durable Object lookup logic
to use the server-side principal/tenant ID so new client-supplied appIDs cannot
spawn unbounded Durable Objects.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Will be fixed later...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@alexeichhorn, understood. Since this is a critical security concern, would you like me to create a GitHub issue to track this authentication enhancement for a future PR? That way it won't be forgotten.

@alexeichhorn
alexeichhorn merged commit a77c6d3 into cloudflare-worker Feb 9, 2026
2 of 4 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