Cloudflare Workers, code that runs close to the user
Cloudflare started as a content delivery network and became a platform capable of hosting an entire application: code, database, file storage, queues, and language models. Code executes in hundreds of locations, so a user in Warsaw does not wait for a response from Virginia.
How it differs from ordinary hosting
Three differences decide whether the platform fits a project.
The first is the execution model. Code does not live in a process waiting for requests, it starts on every invocation and ends after the response. The cold start problem familiar from functions on other clouds disappears, and so does the option of holding state in memory between requests.
The second is the runtime. Workers are not Node, they are an environment built on browser standards. Most modern libraries work, while packages reaching for the filesystem or native modules will not. Check that before rewriting an application, because the incompatibility list tends to be short but contains things you would not expect.
The third is distribution. Code is everywhere, but data has to live somewhere. If the application queries a database in one region, the gain from running code near the user disappears, because you wait for the ocean crossing anyway. That is why the platform added its own data stores, designed around this model.
Your first worker
npm create cloudflare@latest my-app
cd my-app
npm run devRequest handling code uses standard objects, the same ones you know from the browser and from API routes in Next.js.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === '/health') {
return new Response('ok')
}
const result = await env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(url.searchParams.get('id'))
.first()
return Response.json(result ?? { error: 'not found' }, {
status: result ? 200 : 404,
})
},
}The env object holds bindings to resources: databases, stores, queues, and secrets. You configure them in the project settings file rather than through environment variables carrying addresses, which sets this model apart from a typical backend.
{
"name": "my-application",
"main": "src/index.ts",
"compatibility_date": "2026-08-01",
"d1_databases": [
{ "binding": "DB", "database_name": "shop", "database_id": "..." }
],
"kv_namespaces": [
{ "binding": "SETTINGS", "id": "..." }
],
"r2_buckets": [
{ "binding": "FILES", "bucket_name": "attachments" }
],
"queues": {
"producers": [{ "binding": "QUEUE", "queue": "jobs" }],
"consumers": [{ "queue": "jobs", "max_batch_size": 10 }]
}
}The binding name is the only thing the code sees, so swapping a database under the same name requires changing not a single line. The compatibility date on the third line matters more than it looks: it freezes runtime behaviour, so a platform update will not change how your code runs without your decision.
Deployment is one command, and the application appears in every location at once. There is no region to choose, because there is no single place where the code runs.
Data resources and what each is for
The platform has four stores, and confusing their purposes is the most common design mistake.
D1 is a relational database built on SQLite. It suits application data at moderate volume, wherever you need conditional queries and joins between tables.
KV is a key value store optimised for reads. Writes propagate across the network with a delay, so it suits configuration, feature flags, and rarely changing data rather than state requiring immediate consistency.
R2 is file storage compatible with the S3 interface. Its main advantage is no egress charges, which when serving images or downloads makes a larger difference to the bill than the storage price itself.
Durable Objects are single objects with their own state and a guarantee that only one instance of a given object exists at a time. That is the tool for things needing coordination: counters, chat rooms, locks, per user event queues.
Queues complete the set wherever a job need not finish inside the request. A worker accepts the job, drops it into a queue, and answers the user, while a separate process handles it at its own pace.
export default {
async fetch(request: Request, env: Env) {
const { email } = await request.json<{ email: string }>()
await env.QUEUE.send({ type: 'welcome', email })
return new Response(null, { status: 202 })
},
async queue(batch: MessageBatch<Job>, env: Env) {
for (const message of batch.messages) {
try {
await sendWelcome(message.body.email, env)
message.ack()
} catch {
message.retry()
}
}
}
}Acknowledging individual messages rather than the whole batch is what separates a working queue from one that retries nine correctly processed jobs because of a single failure. That is the standard way around the CPU time limit for operations that inherently take longer.
On top of that comes Workers AI, meaning models running on the vendor's infrastructure, billed in compute units, with a free threshold of ten thousand units a day.
What it really costs
| Component | Free plan | Paid plan |
|---|---|---|
| Workers | 100,000 requests a day, 10 ms CPU per invocation | from 5 USD per month per account; includes 10M requests and 30M CPU milliseconds, then 0.30 USD per additional million requests |
| KV | limited daily threshold | 10M reads included, then 0.50 USD per million, writes 5 USD per million |
| D1 | daily threshold | 0.001 USD per million reads, 1 USD per million writes |
| R2 | monthly threshold | 0.015 USD per GB per month, no egress charges |
| Durable Objects | not on the free plan | 0.15 USD per million requests plus duration |
| Workers AI | 10,000 units a day | 0.011 USD per thousand units |
The five dollar headline is a minimum charge rather than a real bill. A production application using workers, a key value store, a database, and files typically costs between fifteen and fifty dollars a month, several times the entry threshold though still modest against a typical server.
Two items tend to surprise. Writes in the key value store cost ten times more than reads, so using it as a write store ends in a bill nobody planned. Durable Objects also bill for duration, so an object kept active around the clock costs more than request count suggests.
It also helps to know the minimum charge covers several services at once, so adding another resource to an existing account does not always raise the bill. While you stay inside the thresholds included in the plan, you pay the same five dollars whether you run one worker or five along with a database and a queue.
The CPU time limit per invocation on the free plan is ten milliseconds, and that, rather than request count, most often forces a move to the paid plan. Ten milliseconds covers a request with one database query, not image processing or document generation.
On the paid plan the default limit is thirty seconds and can be raised to five minutes, though it pays to understand what that limit measures.
{
"limits": { "cpu_ms": 60000 }
}Only processor time counts, not time spent waiting for a database or an external service to answer. A worker waiting two seconds on somebody else's API consumes a fraction of a millisecond of the limit, so raising this value makes sense for data processing rather than for slow network dependencies.
The network layer where all of this started
Before the platform became a place to run code, it was a network in front of your server, and that layer still delivers most of the real value for a typical site.
The content delivery network keeps static files near the user. The default configuration covers images, stylesheets, and scripts, while dynamically generated responses need deliberate rules. A well configured cache can take ninety percent of traffic off the origin server, and on a content site that difference shows in both the bill and load times.
Volumetric attack protection works with no configuration, and it is most often why small sites arrive here at all. The application firewall already demands decisions, since rules blocking too aggressively cut off real users while loose ones protect against nothing.
Domain and certificate management is a separate value. A certificate renews itself, and redirects and URL rewriting rules are set in one place without touching server configuration.
When migrating an existing site, start with this layer and leave the code alone. Switching name servers and enabling caching is a reversible change showing its effect within an hour, and the decision about rewriting the application can wait until real traffic data is visible.
Cloudflare against the alternatives
| Platform | Strength | Weakness | Pick it when |
|---|---|---|---|
| Cloudflare | Code near the user, no R2 egress charges, complete resource set | Runtime is not Node, some libraries will not work | Global application, heavy transfer, cost sensitivity |
| Vercel | Best Next.js support, deployment comfort | Transfer and function cost at high traffic | Next.js project where speed of work is the priority |
| Netlify | Simple deployment model, good static site handling | Less developed data layer | Sites and applications of moderate complexity |
| Your own server | Full control, any library | Maintenance, scaling, and availability fall on you | Native dependencies, data location requirements |
These platforms also combine. A common arrangement runs the application on Vercel with files served from R2, where the absence of egress charges delivers the largest saving without rewriting anything.
The choice usually reduces to two questions. Do your dependencies run outside Node, and is egress a meaningful line on the bill. A yes to the second is often argument enough by itself, since the cost difference when serving files runs into hundreds of dollars a month.
Designing an application for this model
Moving an existing backend across unchanged rarely works out. A few architectural decisions determine whether the platform delivers an advantage or merely complicates the work.
Separate reads from writes. Reads belong as close to the user as possible, from a cache or a read optimised store. Writes can go to one place, since they are rarer and nobody usually notices an extra hundred milliseconds when submitting a form.
Keep state where it belongs. A user session in a signed cookie needs no database query on every request. A counter demanding accuracy is a job for an object with a single instance guarantee, not for a key value store.
Design for short invocations. The CPU time limit forces long operations into stages, and background work belongs in queues. Generating a report inside a user request does not fit this model, while accepting the job and running it asynchronously fits perfectly.
Avoid libraries doing more than you need. A large dependency raises bundle size, and that affects start time on every invocation. On a simple API the difference between two and two hundred kilobytes of code shows up in latency statistics.
The last point concerns testing. The local environment reproduces the platform reasonably faithfully, but cache behaviour and write propagation only appear after deployment. Keeping a separate environment on your own subdomain lets those differences surface before production.
It also pays to decide up front how tightly you want to bind to this platform. Worker code itself rests on standard request and response objects, so it moves across relatively easily, while the data stores are an exclusive part of it and do not exist outside. For a relational database that knot loosens with a compatible external service, Turso for instance, where the same SQLite syntax comes as a separate service reachable over HTTP, so a worker talks to it exactly as it would to any other API. You gain independence from the vendor and lose the database sitting inside the same runtime, with every read becoming a network call.
Common mistakes
The first is treating the key value store as a database. Reads are cheap and fast, but writes cost ten times more and propagate with a delay, so a visit counter written there on every request is a direct route to a surprising bill.
The second is assuming in memory state between requests. A worker may run in a different location on the next invocation, so a global variable is no place for anything durable.
The third is rewriting an application before checking dependencies. One library reaching for a native module can block the whole migration, and you find out after two days of work.
The fourth is ignoring the CPU time limit. Code running locally in fifty milliseconds exceeds the free plan cap, and the error message does not always point plainly at the cause.
The fifth is keeping secrets in the configuration file rather than the secret store. The settings file reaches the repository, while keys should be uploaded with a separate command.
FAQ
Is the free plan enough for production?
For a small project yes, since a hundred thousand requests a day is plenty. The constraint is usually not request count but ten milliseconds of CPU per invocation and the absence of Durable Objects. An application doing anything beyond a simple database read usually needs the paid plan.
What does a typical production application cost?
Usually between fifteen and fifty dollars a month using workers, a database, a key value store, and files. The minimum charge is five dollars, but the real bill is a multiple of that, while still staying low against a typical server with comparable availability.
Does Next.js run on this platform?
Yes, through a dedicated adapter, though some framework features behave differently from the Node environment. On simpler applications the differences go unnoticed, on elaborate ones it pays to check compatibility before deciding to move off Vercel.
How does D1 differ from an ordinary database?
D1 builds on SQLite and is designed around this execution model, so it suits application data at moderate volume. For large datasets and complex queries, Postgres serves better, from Neon for instance, connected through a pooling layer.
Can I run a language model on this platform?
Yes, through Workers AI, where models run on the vendor's infrastructure and bill in compute units, with a free threshold of ten thousand a day. For work needing the strongest models you will still call an external API, Claude for instance.
Documentation sits at developers.cloudflare.com, and pricing in the billing section.