Skip to main content
Middleware runs before your route handler. Use it for CORS headers, rate limits, logging, timeouts, and auth checks. A MiddlewarePipeline chains middleware together and short-circuits to a Response when one rejects the request. The pipeline works in both router styles. The route module wrapper changes:
  • App router API routes live at app/api/**/route.ts and export named HTTP method handlers such as GET or POST. The handler receives the Request directly.
  • Pages router API routes live at pages/api/** and export named HTTP method handlers or a default fallback handler. The handler receives an APIContext as ctx; use ctx.request when a middleware expects a Request.

Prerequisites

  • At least one API route in your project (see API routes).
  • The dev server running so you can hit the routes with curl.

Built-in middleware

CORS

Rate limiting

Forwarded client-address headers are ignored by default. If the app is not behind a trusted reverse proxy, use keyGenerator with a trusted client or account identifier.

Logging

Timeout

Pipeline composition

Combine middleware into a pipeline:

Route-specific middleware

Apply middleware only to matching URL patterns:

Run the pipeline

Use handle() to run the middleware chain and then your route handler. If a middleware short-circuits (returns a Response, for example a rate-limit rejection), handle() returns that response and your handler never runs; otherwise your handler runs as the terminal step:
The same pipeline runs in a pages router handler via ctx.request:
Try it with the dev server running:
The response includes any headers added by the middleware that matched the request.
handle() vs execute(). execute() is a lower-level variant with no terminal handler: it returns the short-circuiting middleware’s Response, or a synthesized 404 Not Found when the chain passes through. It always resolves to a Response (never undefined), so if (await pipeline.execute(request)) is always truthy; use execute() only when a middleware is always expected to produce the response. For the common “middleware, then my route handler” case, prefer handle().

In-memory state across requests

Middleware and route handlers created at module scope, for example a rateLimit() store or a module-level counter, behave differently by environment:
  • In development, the dev server re-evaluates each route module on every request so edits hot-reload. A fresh module scope means module-level variables and default in-memory stores are re-created per request: a counter always reads back its initial value, and the default in-memory rate-limit store never accumulates across requests. To exercise threshold behavior in dev, drive the pipeline multiple times within a single request, or use an external store.
  • In production, the compiled route module is cached per release, so module-scoped state persists across requests within one server process and one release. It is still not shared across multiple instances, and it resets on every redeploy (and under memory-pressure eviction).
For anything that must be correct across requests, instances, and deploys, such as rate limiting, counters, and sessions, use an external store (see the RateLimitStore interface and the Redis example in the rate-limit reference) rather than module-scoped memory.

Cleanup callbacks

Register teardown logic that runs once per request, after the response body has finished, been canceled, or errored:
onTeardown callbacks run for every handle() and execute() call, so a module-scoped route pipeline fires them on each request. For streamed responses, cleanup waits until the body reaches EOF, is canceled by the consumer, or errors. Bodyless, locked, or already-read responses and handler or middleware exceptions clean up before the handle()/execute() promise resolves. Callback errors are logged and swallowed, never surfaced to the client. For long-lived pipelines that need one-shot cleanup on shutdown rather than per request, call pipeline.teardown() explicitly. Unlike the per-request run, teardown() drains and discards the callbacks so they never fire again.

Custom middleware

A middleware is a function that receives a context object and a next function. Access the request via c.request:
Add it to a pipeline:

Project-wide root middleware

Add middleware.ts, middleware.js, or middleware.mjs at the project root to run middleware before every project route. Export one middleware function or an array of functions:
Root middleware has the same ordering and short-circuit contract in local development, dedicated production servers, and the shared hosted runtime. The shared runtime resolves and compiles the file only after it has authenticated the project and selected its release or preview branch. Middleware receives only that request’s project environment through c.env. Production middleware is cached by project, environment, and immutable release or preview branch. Preview cache invalidation reloads the file after source changes, and the cache has a fixed entry limit. A missing file passes through normally. Root middleware runs in front of your project’s routes, not in front of the platform’s. A control-plane dispatch is the signed request the platform sends to your runtime to build a release asset manifest for a deploy, or to start, resume, or cancel a run. It bypasses root middleware and goes straight to the handler that verifies its signature. Middleware could not authorize one of these requests in any case. Infrastructure headers, including the dispatch signature, are withheld from project code, so middleware that gates on a credential rejects the platform’s request to build your own deploy. The signature-keyed bypass is narrow. It applies only to a request that both addresses one of those platform routes and carries the signature header the receiving handler verifies. An unsigned request to POST /api/control-plane/runs/{runId}/execute, an unsigned request to the agents list route, and any other path under /api/control-plane/, including your own routes in that namespace, still run your middleware. Three run-lifecycle routes are a longstanding exception and bypass middleware whether or not they are signed: POST /api/control-plane/runs/{runId}/stream, POST /api/control-plane/runs/{runId}/resume, and DELETE /api/control-plane/runs/{runId}. Do not rely on middleware to gate those three paths. A signed channel dispatch bypasses root middleware on the same terms. This is the request the platform sends to POST /channels/invoke to run one of your agents on a message from Slack or Discord, and it carries its own envelope under its own header, verified by the channel handler rather than by the control-plane signature check. Your middleware does not run for your project’s channel traffic. An unsigned request to POST /channels/invoke still runs your middleware. Production loading is fail-closed. If a declared middleware file cannot be read, compiled, or validated as a middleware export, a dedicated server does not start and a shared server returns an error only for the affected project request. Failed shared loads are not cached, so a corrected deployment can recover without restarting unrelated projects. Development loading remains nonfatal and reports the loading error in the server log.

Verify it worked

Hit a route with and without the headers the middleware expects:
For CORS, include an Origin header and confirm Access-Control-Allow-Origin is set on the response.