Skip to content
UsageRail
Frameworks / Cloudflare Workers

Cloudflare Workers API key checks and rate limits

Run the Hono middleware on Cloudflare Workers with serverless: true: it checks the API key, rate limit and quota before your handler runs, and once the response is ready, it sends the counts with waitUntil. Use @usagerail/node 0.1.2 or later: with earlier versions, Workers could end a request with a 500 when UsageRail was slow to respond.

bash
npm install @usagerail/node hono

The SDK uses node:crypto, so turn on Node.js compatibility:

wrangler.jsonc
{
  "name": "my-api",
  "main": "src/index.js",
  "compatibility_date": "2026-09-01",
  "compatibility_flags": ["nodejs_compat"]
}

Store the app key as a secret with npx wrangler secret put USAGERAIL_APP_KEY, then add the middleware:

src/index.js
import { env } from "cloudflare:workers";
import { Hono } from "hono";
import { usageRail } from "@usagerail/node/hono";

const app = new Hono();
app.use(
  "*",
  usageRail({
    appKey: env.USAGERAIL_APP_KEY,
    auth: "api-key",
    serverless: true,
  }),
);

app.post("/v1/generate", async (c) => {
  const result = await generate(await c.req.json());
  c.get("usageRail").track(result.tokens); // optional: count tokens
  return c.json(result);
});

export default app;

The key comes from the Authorization: Bearer header, or from x-api-key when the request has no bearer token.

What's different from Node.js

  • Read the key at the top of the module. env from cloudflare:workers is available there, so the middleware can be created once and shared by every request on an isolate. Workers don't allow random numbers at the top of a module, so the SDK sets itself up on the first request rather than when the module loads.
  • No snapshotStore and no flushOnExit. A Worker has no file system to save the key list in and no process to hook into. Each new isolate fetches the key list on its first request instead, and counts go out after every response, as described below.
  • Don't import @usagerail/node/node. It's for Node.js processes and needs a file system.
  • Rotating the app key. After a rotation, the old app key keeps working for 24 hours. When only a secret changes, Cloudflare can keep isolates running, and they keep the key they already read. So after wrangler secret put, deploy a change to the Worker's code within those 24 hours, which starts every isolate again with the new key.

How counts get sent

Between requests an isolate can be frozen, so there's no timer running in the background. Instead, after each response, the middleware hands c.executionCtx.waitUntil the work of sending the counts, checking for changes such as revoked keys or new limits, and renewing quota leases:

  • One isolate sends a batch at most every 0.5 seconds. Requests that end meanwhile go out with the next batch, and the last request always takes the remaining counts with it.
  • A batch that fails stays in memory and goes out with the next request.
  • An isolate's first request waits up to 1 second for the key list. If that takes longer, keys are checked with UsageRail one by one until it arrives.
  • If the key list is more than 20 seconds old, a request tries to refresh it first, waiting up to 1 second, so a revoked key normally stops working within about 20 seconds, even on an isolate that sat idle. While UsageRail can't be reached, the isolate keeps using the list it has; see API keys.

If Cloudflare stops an isolate before its waitUntil work finishes, the counts it hadn't sent are lost and never billed. In our October 2026 tests with 0.1.2 on the Workers Free plan that didn't happen: every successful response was counted in a run of 60,000 requests at 100 a second, one of 120 requests a minute apart over two hours, and bursts of up to 260 requests at once on new deployments, which started up to 48 new isolates.

Options

usageRail() takes appKey, auth, consumer, onKeyStoreUnavailable, staleKeySnapshot, onQuotaUnavailable and logger, which work as on the Express page, and onReject, as on the Hono page, plus:

OptionDefaultWhat it does
serverlessfalseSet it to true on Workers. Without it, the Worker fails to start, because the SDK would set itself up while the module loads.

Count units, your own authentication and rejected requests

These work as on Node.js:

  • Units. Call c.get("usageRail").track(n) in the handler with a whole number from 0 to 10,000,000; see quotas.
  • Your own authentication. Register UsageRail after the middleware that identifies the caller and return their ID from consumer, as on the Hono page. consumer has to return right away, not a promise.
  • Rejected requests get the same statuses and JSON body as with Express; see the table on the Express page.

What gets counted

  • Each request is counted under the route template Hono matched, such as /v1/items/:id, its method and its outcome. A request no route handled is recorded as _unmatched.
  • 2xx and 3xx responses are billable. 4xx and 5xx responses and refused requests are recorded but not billed.
  • A server-sent events response counts once, when it starts, and is sent right away. Units you pass to track() during the stream are added when it ends or the client disconnects.
  • A client that disconnects early isn't noticed, so the request is counted by its status.

Rate limits on many isolates

Cloudflare runs your Worker in as many isolates as traffic needs, in many locations, and each one counts rate limits on its own. With the default setting a customer can get the full limit on every isolate. To keep the total close to what you set, choose Split the limit across servers; see rate limits.