Client projects on Cloudflare Workers and D1
What Workers and D1 do well, where their limits are and which kind of project the combination suits. For developers.
9 November 2026 · 5 min read
Two projects I am building now run on Cloudflare. SF Sales Solutions is a careers site and admin app for a sales recruiter, with Astro, React, Workers and D1. Tradi is a website for a menswear agency, with Astro and Workers. Both are still in progress, so I am not writing about results here.
The numbers below come from Cloudflare’s documentation at the time of writing. Check the Workers limits and the D1 limits before you count on them.
What a Worker is
A Worker is a piece of JavaScript or TypeScript that runs on Cloudflare’s network, in a V8 isolate. An isolate starts much faster than a container, and there is no server for you to manage. Your code responds to a request and works with web standards such as fetch, Request and Response.
You reach other services through bindings. You declare them in your configuration, and they arrive in your code as env. You have no connection details to manage.
# Simplified illustration
name = "example"
main = "src/index.ts"
compatibility_date = "2026-10-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "example"
database_id = "..."
Astro fits this well. With the Cloudflare adapter you prerender what can be static, and the rest runs as a Worker. You then use React only on the parts that need it, such as an admin screen.
What Workers do well
There is little to manage. wrangler deploy puts your code online, with no servers to patch or capacity to plan. Your code runs close to the visitor, which mostly helps for whatever needs no database. Local development uses the same runtime as production.
You also have everything in one configuration: D1 for SQL, R2 for files, Queues for background work and Cron Triggers for scheduled tasks. For a site with forms and a small admin screen, that is usually enough.
Where Workers stop
CPU time is limited per request. Waiting on I/O does not count, computing does. On the paid plan you can raise the limit, and on the free plan it is very tight.
An isolate has 128 MB of memory, and one isolate can serve several requests at once. A module-level variable is therefore shared between requests, so do not keep anything there that belongs to one user.
The runtime is not Node. With nodejs_compat, a large part of the Node APIs works, but there is no ordinary file system, and packages with native modules do not run. Check that early if you depend on a library for, say, PDFs or image processing.
Work after the response is possible with ctx.waitUntil, but that has a short limit. Long jobs belong in a queue or a Workflow.
What D1 is
D1 is a managed database based on SQLite. You reach it through a binding, with prepared statements, and you manage the schema with wrangler d1 migrations. With batch you run several statements as one transaction: if one fails, the whole thing rolls back.
// Simplified illustration, not code from a client project.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const form = await request.formData();
const id = crypto.randomUUID();
await env.DB.batch([
env.DB.prepare("INSERT INTO signups (id, name, email) VALUES (?, ?, ?)")
.bind(id, form.get("name"), form.get("email")),
env.DB.prepare("INSERT INTO audit_log (signup_id, action) VALUES (?, 'created')")
.bind(id),
]);
return Response.json({ id }, { status: 201 });
},
};
Where D1 stops
A D1 database is single-threaded and processes one query at a time. So throughput depends directly on how long your queries take, and one slow query holds up all the others. Your indexes help decide how much traffic the database can take.
On the paid plan a database can reach at most 10 GB, on the free plan 500 MB, and you cannot have the 10 GB limit raised. D1 is made to scale out across many smaller databases, for example one per customer. A large shared schema suits that less well.
A batch is a fixed list of statements. You cannot hold a transaction open while your code reads something, decides and then writes. You can often solve that in the SQL itself, with ON CONFLICT or an UPDATE with a version in the WHERE.
A database has one primary location. A Worker far from it pays for that distance on every query, so speed at the edge of the network counts for less once you need the database. With read replication and the Sessions API, reads can go to a replica, while writes keep going to the primary.
If you build for customers in Europe, you will want to know where the data lives. A location hint is only a preference. A jurisdiction, such as eu, keeps the database and its replicas inside that region, and you have to set it when you create the database, because you cannot add it afterwards. The Worker itself can still run anywhere. What that means legally for your project is best put to a lawyer.
Which projects it suits
The combination suits sites with mostly read traffic and forms, and small admin screens with a manageable amount of data. D1 also suits data you can split per customer.
It suits heavy concurrent writing to one database less well, as well as large analytical queries, heavy computation or libraries that lean on Node. Workers can still do the front part then. With Hyperdrive, a Worker can reach an existing Postgres database.
Read next
What's your idea?
Tell me what you're trying to build. I try to reply within one working day.