launchthat
Cloudflare Workers vs Convex: Two Serverless Models, Very Different Trade-Offs
Both Cloudflare Workers and Convex let you run server-side code without managing servers. But they solve fundamentally different problems. After running both in production, here is when I reach for each one — and where they overlap more than you might expect.
I run both Cloudflare Workers and Convex in production. The API proxy that serves api.ltb.it.com is a Cloudflare Worker. The application backend behind it — queries, mutations, real-time subscriptions, scheduled jobs — is Convex. WebSocket relays for real-time collaboration run on Cloudflare Durable Objects. The application state they relay lives in Convex.
On paper, both are "serverless." Both let you deploy TypeScript functions that run on someone else's infrastructure. Both scale to zero when idle and handle concurrency without you managing threads or processes. But the similarities end at the surface. They are built on fundamentally different models, optimized for fundamentally different problems.
This article compares them head-to-head: where they overlap, where they diverge, and when to pick one over the other.
The mental models
Cloudflare Workers is a stateless compute layer at the network edge. Your code runs in V8 isolates distributed across 300+ data centers worldwide. Each request gets its own execution context. There is no built-in database, no built-in auth, no built-in real-time layer. You bring your own state — through KV, D1, R2, Durable Objects, or external services. Workers are general-purpose: you can build anything from a URL redirector to a full API server.
Convex is an integrated application backend. Your code runs as queries, mutations, and actions against a built-in transactional database with real-time subscriptions. The database is not an add-on — it is the foundation. Every query is a live subscription by default. Mutations are transactional. The scheduling system, file storage, and auth integration are all first-party. Convex is opinionated: it is purpose-built for application backends where data consistency and real-time updates matter.
The simplest way to think about it: Workers gives you compute and lets you assemble everything else. Convex gives you an integrated backend and lets you focus on your application logic.
HTTP handlers: the overlap zone
Both platforms can handle HTTP requests. This is where the comparison feels most direct.
Cloudflare Worker:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/health") {
return new Response("ok", { status: 200 });
}
if (url.pathname.startsWith("/api/webhooks/stripe")) {
const body = await request.text();
const sig = request.headers.get("stripe-signature");
// Verify signature, process event, call external DB...
return new Response("received", { status: 200 });
}
return new Response("not found", { status: 404 });
},
};
You own everything: routing, validation, error handling, response construction. There is no framework unless you add one (Hono is the common choice). State lives elsewhere — you fetch it from KV, D1, or an external database.
Convex HTTP action:
const http = httpRouter();
http.route({
path: "/webhooks/stripe",
method: "POST",
handler: httpAction(async (ctx, request) => {
const body = await request.text();
const sig = request.headers.get("stripe-signature");
// Verify signature, then write directly to the database
await ctx.runMutation(internal.stripe.processEvent, {
eventData: body,
});
return new Response("received", { status: 200 });
}),
});
The critical difference is ctx.runMutation. Inside a Convex HTTP action, you have direct access to the database through mutations and queries. There is no network hop to an external data store. The webhook handler verifies the payload, writes to the database, and triggers downstream logic — all within the same platform.
In a Worker, that same flow requires calling out to an external database (D1, Postgres, or a third-party service), adding latency and a failure point.
Winner for HTTP handlers: It depends on where your data lives. If your application state is in Convex, use Convex HTTP actions — you get transactional writes and zero additional latency. If you need edge proximity (CDN-like response times, geo-routing, or request transformation before it reaches your backend), Workers is the right layer.
Background jobs and scheduling
Both platforms can run work asynchronously.
Cloudflare Workers (Cron Triggers):
export default {
async scheduled(event: ScheduledEvent, env: Env) {
// Runs on a cron schedule defined in wrangler.toml
const staleUsers = await env.DB.prepare(
"SELECT * FROM users WHERE last_seen < ?",
)
.bind(Date.now() - 30 * 24 * 60 * 60 * 1000)
.all();
for (const user of staleUsers.results) {
await sendReminderEmail(user.email);
}
},
};
Workers cron triggers run on a fixed schedule. You define the cron expression in wrangler.toml. The handler is a standard function that runs in the same V8 isolate as your fetch handler. State comes from D1, KV, or external sources.
Convex (scheduled functions and crons):
// One-off delayed execution
await ctx.scheduler.runAfter(
5000, // 5 seconds from now
internal.emails.sendWelcome,
{ userId },
);
// Recurring cron
const crons = cronJobs();
crons.interval(
"clean stale sessions",
{ hours: 1 },
internal.sessions.cleanup,
{},
);
Convex has two scheduling primitives: scheduler.runAfter / scheduler.runAt for one-off delayed execution, and cronJobs() for recurring work. Both run server-side Convex functions with full database access.
The key difference is scheduler.runAfter. There is no native equivalent in Workers. If a Convex mutation needs to trigger follow-up work — send an email five seconds after signup, retry a failed payment in ten minutes, process a batch after a delay — you schedule it inline. In Workers, you would need to write to a queue (Cloudflare Queues) or set up a separate cron that polls for pending work.
Winner for scheduling: Convex, by a significant margin. Inline scheduling from any mutation is a capability that eliminates an entire class of infrastructure (job queues, polling workers, state machines for retry logic). Workers cron triggers work well for fixed-interval tasks but lack the dynamic, event-driven scheduling that application backends constantly need.
The database question
This is where the comparison gets asymmetric. Convex has a built-in database. Workers does not.
Cloudflare's data options:
| Service | Type | Best for |
|---|---|---|
| KV | Key-value (eventually consistent) | Config, feature flags, cached responses |
| D1 | SQLite at the edge | Relational data with SQL queries |
| R2 | Object storage | Files, media, backups |
| Durable Objects | Single-instance stateful actors | Coordination, WebSockets, counters |
Each is a separate service with its own API, its own pricing, and its own consistency model. Building an application on Workers means assembling a data layer from these primitives — deciding which data goes where, handling consistency between them, and managing the operational complexity of multiple storage systems.
Convex's data model:
One transactional document database with:
- Automatic indexes
- Real-time subscriptions on every query
- ACID transactions on mutations
- Built-in file storage
- Automatic schema validation
You do not choose between storage backends. You define your schema, write queries, and the platform handles the rest.
Trade-off: Convex's integrated model is dramatically simpler for application backends. You do not think about consistency models or storage tier selection — the database just works. But Cloudflare's composable approach gives you more flexibility for edge-specific patterns. KV's global distribution is unmatched for read-heavy, latency-sensitive data. R2's S3-compatible API slots into existing tooling. D1's SQLite engine lets you run complex queries at the edge without a round-trip to a centralized database.
If your primary concern is application data — users, posts, transactions, relationships — Convex's integrated approach eliminates significant complexity. If you need globally distributed reads with eventual consistency, or you are building infrastructure-level tooling rather than an application, Cloudflare's composable primitives give you the right building blocks.
Real-time: subscriptions vs Durable Objects
Both platforms support real-time patterns, but the mechanisms are completely different.
Convex subscriptions are automatic. Every query is a live subscription. When the underlying data changes, every connected client receives the update. You do not set up WebSocket servers, manage connection state, or build pub/sub infrastructure. The reactivity is built into the query layer.
// This query is automatically reactive
const messages = useQuery(api.messages.list, { channelId });
// 'messages' updates in real-time when any mutation changes the data
Cloudflare Durable Objects provide stateful, single-instance actors with WebSocket support. Each Durable Object instance runs in one location, maintains in-memory state, and can handle WebSocket connections directly.
export class ChatRoom extends DurableObject {
connections: Set<WebSocket> = new Set();
async fetch(request: Request): Promise<Response> {
const pair = new WebSocketPair();
this.ctx.acceptWebSocket(pair[1]);
this.connections.add(pair[1]);
return new Response(null, { status: 101, webSocket: pair[0] });
}
async webSocketMessage(ws: WebSocket, message: string) {
for (const conn of this.connections) {
if (conn !== ws) conn.send(message);
}
}
}
I use Durable Objects in production for WebSocket relay infrastructure — presence indicators and transport channels for real-time collaboration. The Durable Object manages the WebSocket connections and broadcasts messages. But the canonical state — who is present, what the current document looks like, what messages have been sent — lives in Convex.
This is the key insight: Durable Objects excel at connection management and ephemeral coordination. Convex excels at persisted state with automatic reactivity. They solve different layers of the real-time problem.
If you need real-time updates driven by database changes (a new message, a status change, a record update), Convex subscriptions handle it with zero additional infrastructure. If you need raw WebSocket control — custom protocols, binary message framing, connection-level state — Durable Objects give you that low-level access.
In my architecture, they work together: Durable Objects handle the WebSocket transport layer, and Convex handles the state layer. Presence heartbeats flow through the Durable Object for low-latency broadcasting. The authoritative presence state is periodically synced to Convex for persistence and querying.
Developer experience
Cloudflare Workers gives you a blank canvas. wrangler dev starts a local development server. You write a fetch handler and build from there. The ecosystem is rich — Hono for routing, Zod for validation, drizzle-orm for D1 — but you assemble it yourself. The flexibility is the value and the cost.
A typical Worker project structure:
src/
index.ts ← fetch handler + routing
routes/
health.ts
webhooks.ts
lib/
auth.ts
db.ts ← D1/KV/DO bindings
wrangler.toml ← config, routes, bindings
Convex gives you a structured framework. npx convex dev starts a dev server that watches your convex/ directory for changes, validates your schema, and hot-deploys functions. The file-based routing, argument validators, and return type validators enforce conventions that prevent entire categories of bugs.
A typical Convex project structure:
convex/
schema.ts ← the entire data model
messages.ts ← queries + mutations for messages
users.ts ← queries + mutations for users
http.ts ← HTTP endpoints
crons.ts ← scheduled jobs
The Convex developer experience is more constrained but dramatically faster for application development. You do not decide how to structure your API — you write functions and export them. You do not choose a validation library — validators are built in. You do not configure a database connection — the database is there.
Trade-off: If you want full control over your runtime, Workers is the better choice. If you want to move fast on application features and are willing to accept Convex's conventions, Convex will get you to production faster.
When I reach for each one
After running both in production across multiple projects, my decision framework is straightforward:
Use Cloudflare Workers when:
- You need edge proximity — the code must run close to the user for latency reasons
- You are building infrastructure, not application logic — proxies, redirects, request transformation, API gateways
- You need raw WebSocket control with custom protocols (Durable Objects)
- You want to compose your own stack from low-level primitives
- Your data lives in Cloudflare's ecosystem (KV, D1, R2) or external services
- You are building something that does not need a traditional database — middleware, edge caching, header manipulation
Use Convex when:
- You are building an application backend with persistent data and complex relationships
- You need real-time reactivity out of the box
- You want transactional guarantees on your writes
- You need dynamic scheduling (delayed jobs, retries, event-driven follow-up actions)
- You want the database, API, auth, file storage, and scheduling in one platform
- Development speed matters more than infrastructure flexibility
Use both when:
- You need edge-level request processing before traffic reaches your application backend
- You want branded API domains without Convex custom domain pricing (this is exactly what my API proxy Workers do)
- You have real-time collaboration features where WebSocket transport and persistent state are separate concerns
The cost comparison
Pricing models differ enough that direct comparison requires context:
| Cloudflare Workers | Convex | |
|---|---|---|
| Free tier | 100k requests/day | 1M function calls/month |
| Compute pricing | $0.30/million requests (Paid plan) | Included in plan |
| Database | D1: 5M rows read free, then $0.001/M | Included (document-based) |
| Real-time | Durable Objects: $0.15/million requests | Included (automatic subscriptions) |
| File storage | R2: 10GB free, $0.015/GB/month | Included in plan |
| Scheduling | Cron Triggers: included | Included |
| Paid plan | $5/month (Workers Paid) | $25/month (Pro) |
For a typical application backend, Convex's bundled pricing is simpler to reason about. You do not pay per storage system, per request type, or per real-time connection. For edge infrastructure and proxy layers, Workers' per-request pricing is extremely competitive — most proxy workloads stay well within the free tier.
The bottom line
Cloudflare Workers and Convex are not competitors in the way that, say, two database products compete. They occupy different layers of the stack and solve different categories of problems.
Workers is compute infrastructure — fast, flexible, globally distributed, and unopinionated. It gives you the raw materials to build anything, but you build everything yourself.
Convex is an application platform — integrated, reactive, transactional, and opinionated. It gives you a complete backend, but you build within its model.
In my production stack, they coexist without friction. Workers handles the edge — proxying, transport, request transformation. Convex handles the application — data, logic, real-time updates, scheduling. The boundary between them is clean because they are not trying to do the same thing.
The question is not "which one is better." The question is "what are you building?" If the answer involves persistent data, complex queries, real-time updates, and transactional writes — start with Convex. If the answer involves edge compute, custom protocols, or infrastructure-level request handling — start with Workers. And if you need both, they compose well together.
Want to see how this was built?
See how they work together in the Portal projectWant to see how this was built?
Browse all posts