Cloudflare
Pages Functions
Workers
API Routes
Serverless

Pages Functions: when to use instead of pure Workers

Co-locating frontend and API in the same repository with automatic preview deployments is the core case of Pages Functions, not a technical limitation of the platform.

Pages Functions: when to use instead of pure Workers

There is a common misconception about Pages Functions: that they are a simplified or limited version of Workers, suitable for trivial cases and insufficient for serious production. That's not true. Pages Functions run on the same V8 runtime as Workers, access the same bindings, respect the same limits. The difference between the two forms of deployment is not one of capacity — it is one of operational context.

What Pages Functions are, technically

Pages Functions are Workers deployed via file system convention within a Pages project. You create a directory /functions in the root of the repository, and each TypeScript or JavaScript file there becomes a route. File /functions/api/users/[id].ts responds in /api/users/:id. File /functions/webhooks/stripe.ts responds in /webhooks/stripe.

The runtime that executes this code is identical to that of a standalone Worker: V8 isolates, cold start below 1ms, 128MB of memory per invocation (hard limit), 30 seconds of CPU per request on the paid plan, 1,000 subrequests per invocation. The available bindings are the same: D1 for SQLite database at the edge, KV for key-value storage, R2 for objects, Durable Objects for consistent state, AI for inference, Service Bindings for direct calls to other Workers.

The distinction exists in deployment: a Pages Function is created as part of a Pages project that also has static assets. It is not possible to deploy a Pages Function without a Pages project. This is not a technical limitation — it is a product choice that defines when it makes sense to use one or the other.

When Pages Functions win

The strongest case for Pages Functions is co-location: frontend and backend in the same repository, with the same deployment cycle. A Next.js, Astro, or SvelteKit project with API routes is naturally co-located — you change a component and the API route it consumes in the same commit, the same pull request, the same preview deployment.

This preview per branch is the most concrete operational differentiator. Each push to any branch generates a preview URL with the format hash-nome-da-branch.seuproject.pages.dev, where both static assets and Functions are running. This means that the PR reviewer can test the complete feature — interface and API — without deploying it to any separate environment. Pure Workers don't have this flow natively.

If your API routing naturally maps to URL paths and you don't need complex routing logic outside of the directory structure, Pages Functions eliminate the need for a separate Worker with its own routes configured in wrangler.toml.

The directory structure and middleware pattern

A realistic Pages with Functions project structure:

/functions
  _middleware.ts          ← executa antes de toda Function no diretório
  /api
    _middleware.ts        ← executa antes de toda Function em /api
    users/
      [id].ts             ← GET /api/users/:id, PUT /api/users/:id
      index.ts            ← GET /api/users, POST /api/users
    webhooks/
      stripe.ts           ← POST /api/webhooks/stripe

The _middleware.ts file is a composition mechanism that many people ignore. It receives the request before the path-specific Function is executed, and can short-circuit it with its own response or pass it on to the next handler via ctx.next(). This is for authentication, centralized logging and CORS without repeating logic in each Function:

// /functions/api/_middleware.ts export async function onRequest(ctx: EventContext<Env, any, any>) { const token = ctx.request.headers.get("Authorization"); if (!token || !isValidToken(token, ctx.env.JWT_SECRET)) { return new Response("Unauthorized", { status: 401 }); } const response = await ctx.next(); response.headers.set("X-Content-Type-Options", "nosniff"); return response; }

The middleware in the root /functions covers all Functions. The middleware in /functions/api only covers API routes. You can stack the two — the one in the parent directory runs first.

When pure Workers are the right choice

Pages Functions do not support cron triggers. If you need a job that runs at 3am to process billing, sync an external feed, or clean up expired sessions, that needs to be a standalone Worker with [triggers] crons on wrangler.toml. There is no alternative within the Pages model.

Queue consumers — Workers that process Cloudflare Queues messages asynchronously — also do not exist in Pages. If your architecture uses queues to decouple heavy processing from the request's critical path, the consumer needs to be a separate Worker.

Workers for Platforms, the dispatch mechanism for user-deployed scripts (in the case of multi-tenant SaaS where each customer has its own code), is exclusive to Workers. Email Workers, who receive and process incoming emails, ditto.

The rule of thumb: if the trigger is not an HTTP request, it is a pure Worker. Pages Functions are exclusively HTTP.

Sharing code between separate Pages Functions and Workers

A common architecture uses both: Pages project for the frontend and the main API in /functions, plus separate Workers for background jobs. The problem is that the business code may need to be shared — data validation, D1 database access, authorization logic.

The solution is internal npm packages (using npm/pnpm workspaces) or a dedicated Worker as a "service layer" accessed via Service Binding by others. Service Binding allows a Pages Function to call a Worker directly, on the same internal Cloudflare network, at no network cost and without going through the public internet:

// /functions/api/orders/index.ts export async function onRequestPost(ctx: EventContext<Env, any, any>) { // Chama o Worker de processamento via Service Binding const result = await ctx.env.ORDER_PROCESSOR.fetch( new Request("https://internal/process", { method: "POST", body: ctx.request.body, }) ); return result; }

The ORDER_PROCESSOR here is a standalone Worker that has access to queues, crons, and any other primitives that Pages Functions do not support. Pages Functions are at the HTTP layer; Autonomous workers sit in asynchronous triggers. The two share D1 and KV bindings pointing to the same resources.

The decision criteria

If you're building a project with a frontend — anything that generates static assets at build — start with Pages. The Functions you add in /functions have exactly the same power as a standalone Worker for HTTP cases. You get branch preview and integrated build pipeline without paying any additional operational costs.

If you are building a service without a frontend, with non-HTTP triggers, or that requires named environments with different bindings for staging and production, Workers is the most direct path. The explicit configuration in wrangler.toml is more auditable and the deployment model is more flexible for pure backend services.

The two are not mutually exclusive. Most serious projects end up with both: Pages for what is HTTP and co-located with the frontend, Workers for what is asynchronous or scheduled.

Also read