All jobs

Add Rate Limiting to a Cloudflare Worker

Yes. An agent adds a Workers rate limiting binding, a Hono middleware that answers 429 past the limit and a Vitest case that proves it, then opens a pull request.

Run this on RushIn a Project that has your repository.

What the agent does

The agent clones the Worker into its own cloud sandbox and reads its configuration before it writes any limit. In a Worker like the chanfana-openapi-template in cloudflare/templates, wrangler.jsonc sets compatibility_date to 2025-10-08, main to src/index.ts, upload_source_maps and observability on, and one d1_databases binding named DB. package.json pins hono 4.11.1, chanfana 2.8.3, zod 3.25.67, wrangler 4.136.1, vitest 4.1.10 and @cloudflare/vitest-pool-workers 0.21.3, and its test script is wrangler deploy --dry-run && npx vitest run --config tests/vitest.config.mts. src/index.ts builds a Hono app, hangs an onError handler that answers chanfana's ApiException with its own status and anything else with { success: false, errors: [{ code: 7000, message: "Internal Server Error" }] } and 500, wraps the app with fromHono for the OpenAPI document at /, and mounts tasksRouter at /tasks and DummyEndpoint at POST /dummy/:slug.

The limit itself is a Workers rate limiting binding, which needs no KV namespace or Durable Object. The agent adds a ratelimits entry to wrangler.jsonc with a name such as RATE_LIMITER, a namespace_id given as a string holding an integer, and simple with a limit and a period, which Cloudflare accepts only as 10 or 60 seconds; the pinned Wrangler is past the 4.36.0 that introduced the binding. It runs npm run cf-typegen, which is wrangler types, so worker-configuration.d.ts gains the binding on Env and src/types.ts's AppContext sees it without a hand-written declaration.

In src/index.ts it adds a Hono middleware ahead of the routers: it reads the client address from the cf-connecting-ip header, calls c.env.RATE_LIMITER.limit({ key }), and when success is false answers 429 in the same { success: false, errors: [...] } shape the error handler uses, with a Retry-After header equal to the period, so API clients that already parse chanfana errors need no new case. The OpenAPI document at / stays unlimited, and the middleware is scoped to /tasks/* and /dummy/*. The limit values are read from the issue or the prompt; the agent does not pick a number for your API.

The test goes under tests/integration/, next to tasks.test.ts, which already drives the Worker through SELF.fetch from cloudflare:test with the D1 migrations applied by tests/apply-migrations.ts. The new file sends one more request than the limit allows to the same path with the same address header and asserts that the last answer is 429 with the error body and the Retry-After header, and that a request with a different address still passes. It lives in its own file so the counters it exhausts belong to it. The agent runs npm run test, whose first half is a wrangler deploy --dry-run that validates the new binding before Vitest starts, and npm run schema to confirm the OpenAPI output is unchanged.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Add rate limiting to this Cloudflare Worker. Read wrangler.jsonc, package.json, src/index.ts, src/types.ts, the routers under src/endpoints and tests/vitest.config.mts. Add a ratelimits binding to wrangler.jsonc (name, namespace_id as a string, simple.limit and simple.period of 10 or 60) with the limit and period given after this prompt or in the issue that follows it; do not choose the numbers yourself, and if none are given stop and say so. Run npm run cf-typegen so worker-configuration.d.ts includes the binding. In src/index.ts add a Hono middleware on /tasks/* and /dummy/* that keys on the cf-connecting-ip header, calls the binding's limit({ key }), and on failure answers 429 with the same { success: false, errors: [...] } shape the onError handler uses plus a Retry-After header; leave the OpenAPI document at / unlimited. Write tests/integration/rate-limit.test.ts using SELF.fetch that sends one request more than the limit with one address and expects 429 with that body and header, and that a second address still passes. Run npm run test (which starts with wrangler deploy --dry-run) and npm run schema. Open a pull request that quotes the test output and the binding block.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is a pull request with four changed files: wrangler.jsonc with the ratelimits block, worker-configuration.d.ts regenerated by wrangler types, src/index.ts with the middleware, and a new tests/integration/rate-limit.test.ts. The description quotes the binding block, the 429 body, and the vitest run summary from the sandbox, including the wrangler deploy --dry-run line that validated the configuration.

A reviewer checks that the middleware's key is the right one for the deployment: cf-connecting-ip is the caller for a Worker on a public hostname, but an API behind another Worker or a proxy may need a different header or an API key, and the pull request says which it used. They also confirm the period is 10 or 60 and the namespace_id does not collide with another binding's, since bindings that share a namespace share a limit.

The transcript keeps cf-typegen, the test run and the schema check with their output, so a reviewer who wants to know whether the OpenAPI document changed reads the diff of npm run schema rather than regenerating it. If the repository is the templates monorepo itself, the root pnpm run check runs Prettier, syncpack and the templates linter, and the agent runs it before pushing.

Run it on every Linear issue labelled agent

In the console, add an automation to the Project with this prompt and a linear trigger on linear.issue.labeled, filtered to the agent label. When someone labels an issue such as "a single client is hammering POST /tasks", the trigger puts the issue, with the limit and period it names, after the prompt and starts a run in the Worker's repository; the pull request links back to the issue. Only label changes made by a person in your Rush org fire it.

The same body can be sent from your own tooling with POST /o/{org}/sessions, the repository in resources and the issue's URL beside it, read through your Linear connection. A request_id made from the issue id makes a retried call return the run it already started.

The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.

Why run it on Rush?

A rate limit on a Worker touches three places that must agree: the binding in wrangler.jsonc, the generated Env type, and the middleware that calls it, and the only honest check is a dry-run deploy plus a test that actually exhausts the limit. The agent does all of that in its own sandbox with the repository's pinned Wrangler and Vitest, and the pull request quotes the output.

The result is a diff a reviewer can read in a minute, with the proof attached and the one judgement call, which header to key on, stated rather than buried. Several Workers can get the same treatment at once, each in its own sandbox, and a follow-up on the same session changes the key or the scope without starting over.

Frequently asked questions

Why a rate limiting binding rather than KV or a Durable Object?

The binding is built into the Workers runtime and needs no storage namespace, no extra billing line and no cleanup of stale counters. Its limits are approximate and eventually consistent, which Cloudflare documents, so it is right for protecting an API from abuse and wrong for metering or billing. The pull request says so, and a Durable Object design is a different job.

Can the period be 30 seconds or a minute and a half?

No. The simple configuration accepts a period of 10 or 60 seconds only, and wrangler deploy --dry-run refuses anything else before the tests start. If the issue asks for another window, the agent picks the nearer of the two, scales the limit to match, and says so in the pull request.

Does the test really hit the limit, or mock it?

It really calls the binding: @cloudflare/vitest-pool-workers runs the Worker in Miniflare, which implements the rate limiting binding, and the test sends one request more than the limit through SELF.fetch. The agent keeps the test in its own file so that exhausting the limit does not leak into tasks.test.ts, and checks the pinned pool version's behaviour between tests before relying on it.

Will the OpenAPI document change?

Not unless you ask for the 429 response to be documented. The middleware runs before chanfana's endpoints and the document at / is excluded from the limit, so npm run schema produces the same openapi.json. Adding a documented 429 response to each endpoint is a follow-up on the same session.