We have run both models in production: a fleet of Node microservices on Kubernetes, and a set of edge Workers backed by managed storage. The second is dramatically less work to operate. It is not strictly better, and the differences are specific enough to be worth writing down.
What genuinely got better
- No cluster. No node pools, no ingress controllers, no certificate renewals, no 3am page because a node ran out of disk.
- Deploys measured in seconds, which changes behaviour — small changes ship immediately instead of accumulating into a risky batch.
- Cost that tracks usage rather than provisioned capacity. A low-traffic marketing site costs approximately nothing instead of a minimum viable cluster.
- Latency that is genuinely global without running regions, which matters more for a consumer product than a B2B one.
What genuinely got harder
The runtime is not Node. Most libraries work; the ones that reach for filesystem or raw sockets do not, and you find out at deploy time. Long-running work has to be restructured into queues or scheduled invocations rather than a process that just keeps going.
Connection-oriented databases sit awkwardly with a runtime that scales to thousands of short-lived isolates. Managed data services designed for the model work well. A traditional pooled Postgres connection from an edge function is a footgun that will find you under load.
The one that cost us a day
A Worker calling another Worker in the same account by its public hostname does not behave like an external HTTP call. When both are served from the same edge, requests through the public route can fail rather than loop back cleanly — we saw connection errors that looked, misleadingly, like the target service being down.
The correct mechanism is a service binding: declare the target in configuration and call it directly, bypassing the network entirely. It is faster, it is not billed as an extra request, and it does not depend on DNS.
# wrangler.toml — bind the API worker instead of fetching its hostname
[[services]]
binding = "API_WORKER"
service = "my-api"The general lesson: on a platform where the network topology is unusual, reach for the platform's own primitive rather than the generic one. The generic one often appears to work in development and fails in the deployed topology.
How we choose now
Edge-first for anything request-shaped: APIs, sites, webhooks, scheduled jobs, static delivery. It removes an entire operational discipline from the project, and for a small team that is the difference between shipping features and maintaining infrastructure.
Containers or a managed runtime when the work is genuinely long-running, needs a specific native dependency, or has a memory profile the edge will not accommodate. That is a real category, it is just much smaller than most teams assume before they measure it.