Domdhi logoDOMDHI.OS
TECH

Cloudflare Workers: The Edge Runtime That Actually Works

310,360 lines, 13 modules, 126 API endpoints, SSR on every request, deploying in about ninety seconds. No containers, no cold start problem, and a bill that rounds to nothing. The catch is real though, and it's not performance -- it's that you can't use Node.

DDominic BacaSOLO ENGINEER · DOMDHI.OS2026.11.085 MIN READ
TECH// FIG.01

The entire platform — 310,360 lines of TypeScript, 13 modules, 126 API endpoints, server-side rendering on every request, cron jobs, an email handler — runs as one Cloudflare Worker.

git push and it's live in about ninety seconds. No Dockerfile. No Kubernetes. No autoscaling policy. The monthly bill rounds to approximately nothing.

I want to explain why that works, and then be honest about the thing that makes it genuinely hard.

Isolates, Not Containers

This is the architectural difference everything else follows from.

A traditional serverless function boots a container with a full runtime. That's the cold start — hundreds of milliseconds to seconds, and it's why so much serverless tooling is fundamentally about avoiding cold starts: provisioned concurrency, keep-warm pings, all of it working around the model rather than with it.

Workers run on V8 isolates — the same mechanism that separates tabs in Chrome. An isolate is a sandboxed JavaScript context, and spinning one up is on the order of single-digit milliseconds because there's no OS process, no container image, no runtime boot. One process hosts many isolates.

The practical consequence: cold starts stop being a design consideration. Not "minimized" — removed from the set of things you think about. Every architectural workaround built to dodge them becomes unnecessary, and a surprising amount of serverless complexity is exactly those workarounds.

Second consequence: it runs in every Cloudflare location by default. There's no region to pick. Your code is near the user because there's no meaningful cost to it being everywhere.

The Constraint That Defines Everything

Workers are not Node.js.

They implement web-standard APIs — fetch, Request, Response, URL, crypto.subtle, TextEncoder, streams. They do not give you Node's runtime. No fs. No net. No raw Buffer semantics you can lean on. No native modules.

That single fact determines what you can build, and it's where most people bounce off.

What it costs in practice:

Library selection narrows. Anything touching the filesystem, raw TCP sockets, or native bindings is out. A huge amount of the npm ecosystem assumes Node. You check compatibility before adopting, every time, and sometimes the answer is no and you write it yourself.

Crypto is WebCrypto. crypto.subtle is async and lower-level than Node's crypto. Field encryption is PBKDF2 + AES-GCM through WebCrypto, which is fine but means porting any example code you find.

Database access is HTTP. No persistent TCP connection pool — you can't hold a Postgres connection across an isolate. Supabase works because PostgREST is HTTP. A direct pg connection is not an option, and if your database only speaks raw TCP you need a proxy or a different platform.

I don't experience this as a limitation so much as a filter. It rules out a category of application entirely, and for everything left it pushes you toward smaller dependencies and web standards, which age better anyway. But if your app needs Node, this is a hard no rather than a workaround, and you should find that out on day one rather than three months in.

The Limits You'll Actually Hit

Published limits are one thing; which ones bite is another.

CPU time per request. Generous on the paid plan and effectively irrelevant for request/response work. Rendering pages, querying, transforming JSON — never close. You'd hit it doing heavy computation, which you shouldn't be doing in a request anyway.

Bundle size. This is the one that actually constrains me. Everything ships in one bundle, so every dependency is a tax paid on deploy size forever. It has genuinely changed which libraries I adopt — I've written small utilities rather than pull a large package more than once. Annoying, and honestly it's made the codebase better.

Memory per isolate. Fine unless you're buffering large payloads. Stream instead of buffering and it's a non-issue.

No mutable global state between requests. Isolates are recycled unpredictably. Anything you stash in a module-level variable may vanish or be shared in ways you don't control. State goes in KV, D1, Durable Objects, or your database — never in a global. This catches people migrating from long-lived Node servers where an in-memory cache "just worked."

What The Platform Gives You For Free

The part that's easy to undersell: it isn't just compute.

The same deploy gets KV (edge key-value), D1 (SQLite), R2 (S3-compatible object storage with no egress fees), Durable Objects (consistent stateful coordination), Queues, cron triggers, and email routing. All in one wrangler.jsonc, all with typed bindings on env.

The cron triggers run the scheduled publisher for this blog plus reminders, cleanup, and portfolio sync — declared as two lines of config, no scheduler to operate. The email handler receives inbound mail as a worker invocation.

Assembling that from separate services would be a meaningful chunk of infrastructure work and an ongoing operational surface. Here it's configuration.

One Gotcha Worth The Price Of Admission

wrangler types generates a typed Env from your config — and it types a vars value as its literal. So "true" isn't string, it's the literal type "true".

Which means this fails to compile:

if (env.SOME_FLAG === "true") { ... }   // TS2367: no overlap

...because TypeScript has narrowed it to a single literal and the comparison is provably constant. Read flags as String(env.SOME_FLAG) === "true" instead.

It only surfaces on a full tsc run, not in a scoped vitest, which is exactly the kind of bug that lands in CI instead of locally.

Would I Pick It Again

Yes, without hesitating, for this shape of application — request/response, SSR, HTTP-based data access, globally distributed, no long-running processes.

I would not pick it for something needing Node libraries, persistent connections, long-running background compute, or a filesystem. That's not a flaw; it's a different tool.

The stack works because every piece of it agreed to the same constraint: no Node, HTTP everywhere, deploy as one unit. Constraints that specific either fit your problem exactly or not at all.

Mine fit. Ninety seconds, one command, live everywhere.

GET_TRANSMISSIONS

Raw signal on finance, freedom, fitness, and tech. No spam. Unsubscribe anytime.

D
Dominic Baca

One engineer building Domdhi.OS in public — money, freedom, and the occasional 2am migration. Every number live, every commit public.

FOLLOW THE LOGS →
// NEXT IN THE LOGS