# 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 Rush](https://cloud.getrush.ai/run/add-rate-limiting/cloudflare-workers/), in 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`

```json
{
  "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.

## 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.
