all posts

The best Express and Node.js hosting platforms in 2026

Ajay Kumar··8 min read

An Express app is a single Node process holding an event loop. That one sentence explains most of what goes wrong when you host one: a single process means one CPU core unless you fan out, an event loop means one slow synchronous call stalls every concurrent request, and a long-lived process means platforms designed around short-lived invocations are a poor structural fit even when they technically run your code.

Sort platforms by whether they respect that shape. I build PandaStack, one of the options below — flagged where relevant.

Four properties that pick your category

  • Do you hold connections? WebSockets, server-sent events, long polling. If yes, you need a long-lived process with sticky routing, and most function platforms are out.
  • Do you do CPU work in-process? Image resizing, PDF generation, crypto, big JSON transforms. If yes, you need worker threads or a separate service, because that work blocks every other request on the loop.
  • Do you have background jobs or cron? If yes, you need somewhere for a second process to live.
  • Is traffic spiky and request-scoped? If yes — and the answers above are all no — functions are genuinely cheaper and you should take the win.

The categories

1. Serverless functions and edge runtimes

Lambda, Cloud Functions, Vercel functions, Cloudflare Workers. You can run Express here — adapters exist that wrap the app and translate an invocation into a request — but you are running a server framework inside something that isn't a server, and it shows at the edges: no sockets, no in-process background work, no shared in-memory state, and a cold start whose cost scales with your dependency tree.

Cloudflare Workers deserve a separate note: they run V8 isolates rather than Node, so native modules and parts of the Node API need checking. Extremely fast and cheap when the app fits, a rewrite when it doesn't.

2. Container PaaS

Render, Railway, Fly.io, Heroku, Koyeb, Northflank, DigitalOcean App Platform. Push a repo, they detect Node from package.json, install, and run your start script as a long-lived process. Sockets work. Background workers are a second service. Cron is a checkbox. This is the default answer for an Express API and it's the right one most of the time.

Things to check: whether the platform's health check hits a path your app actually serves, whether the proxy in front supports WebSocket upgrades, and whether the idle-scaling behaviour matches your latency expectations.

3. A VM or VPS you manage

Still a completely reasonable answer for a small API. You get full control and predictable pricing, and you take on OS patching, TLS renewal, process supervision, and deploys. The honest trade: a VPS is cheapest in cash and most expensive in attention. Fine for one service, tedious at five.

4. MicroVM platforms

Fly Machines, PandaStack. A PaaS-shaped deploy onto a hardware-isolated VM with its own kernel. My category. Worth it when the API executes code it didn't write — a webhook transformer running customer JavaScript, an agent backend evaluating generated code — or when you need kernel-level capabilities like nested containers. Also relevant if the API's job is to create environments per request; on a snapshot-restore substrate that's roughly 179ms at p50 per machine.

The thing that actually improves your throughput

Before changing platforms because Node is 'slow', check whether you're using the whole machine. One Node process uses one core. On a two-core instance you are, by default, wasting half of it.

// Bind to the port the platform gives you, and to 0.0.0.0 — not localhost.
// An app listening on 127.0.0.1 is invisible to the platform's proxy and
// fails health checks while looking perfectly healthy from inside the box.
const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0", () => console.log("listening on " + port));

// Graceful shutdown: without this, a deploy cuts in-flight requests.
process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 10_000).unref();
});
Binding to localhost instead of 0.0.0.0 is the most common reason a Node deploy 'works' but returns 502. The process is up, the logs are clean, and nothing outside the machine can reach it. Health checks that probe from outside catch this; health checks that run inside the container do not.
# Node version pinned in the repo travels with the code
echo "22.11.0" > .nvmrc

# Deploy from git; install and start are read from package.json
pandastack apps create --name orders-api \
  --git-url https://github.com/acme/orders-api \
  --start-cmd 'node dist/server.js'

If you hold WebSockets, read this part

Socket apps break differently. Three things to verify on any platform before you build on it:

  1. Does the ingress proxy pass the upgrade handshake through, and what is its idle timeout? A 60-second idle timeout silently kills connections unless you send heartbeats under it.
  2. Is routing sticky? With more than one instance and no sticky sessions, a reconnect lands somewhere with no memory of the session. Socket.IO's polling fallback fails loudly here; raw WebSockets fail quietly.
  3. What happens to open connections during a deploy? Blue-green with a drain period lets clients reconnect gracefully. A hard cut disconnects everyone at once, which your reconnect logic had better handle.

The short version

A normal Express API with a database: a container PaaS, cluster across the cores you're paying for, and set up graceful shutdown on day one. Spiky, request-scoped, no sockets or background work: functions, and enjoy the bill. One small service and you like servers: a VPS. Executing untrusted code, needing kernel capabilities, or spawning environments per request: microVM platforms, mine included. Most Node hosting problems turn out to be the port binding, the process count, or the shutdown handler — not the platform.

Frequently asked questions

Can I run Express on serverless functions?

Technically yes, with an adapter that translates an invocation into a request and back. Whether you should depends on what your app does between requests. You lose WebSockets and server-sent events, in-process background work, in-memory caches and rate limiters shared across requests, and any assumption that state survives from one request to the next. You also pay a cold start proportional to your dependency tree, which for a typical Express app with an ORM and a validation library is not trivial. For a stateless CRUD API with spiky traffic it works well and costs less. For anything holding connections or doing work between requests, a long-lived process is the correct shape.

Why does my Node app return 502 even though the logs look fine?

Nine times out of ten the app is listening on 127.0.0.1 instead of 0.0.0.0, so it is reachable from inside the container and invisible to the platform's proxy. The logs say 'listening on 3000' and everything looks healthy. The other common causes are listening on a hard-coded port instead of the one the platform supplies in PORT, and a health check pointed at a path the app doesn't serve, which makes the platform mark a working instance unhealthy and pull it out of rotation. Check the bind address first — it costs ten seconds and is usually the answer.

Do I need Node's cluster module or PM2 on a hosting platform?

It depends on whether you're paying for more than one core. A single Node process uses one core, so on a two- or four-core instance you're leaving capacity idle unless you fan out with cluster, run several instances behind the platform's load balancer, or use a process manager. On platforms that give each instance one core, clustering adds memory overhead for nothing and running multiple small instances is cleaner. Whichever you choose, make sure only one of them runs migrations and scheduled jobs — duplicated cron across four workers is a classic and expensive bug.

Will WebSockets work on my hosting platform?

Check three things rather than trusting a feature list. First, does the ingress proxy pass through the upgrade handshake, and what is its idle timeout — a 60-second timeout kills idle connections unless your heartbeat interval is comfortably under it. Second, is routing sticky across instances; without stickiness a reconnect lands on an instance that has never heard of the session. Third, what a deploy does to open connections — a blue-green flip with a drain period lets clients reconnect in an orderly way, while a hard cut drops everyone simultaneously and your reconnect logic had better include backoff and jitter.

How do I stop deploys from dropping in-flight requests?

Handle SIGTERM. On deploy, the platform sends SIGTERM and then, after a grace period, SIGKILL. Without a handler, Node exits immediately and every in-flight request is cut. The correct sequence is: stop accepting new connections with server.close(), let existing requests finish, close database pools and message consumers, then exit — with a timeout that force-exits if something hangs, so you don't sit there until SIGKILL arrives. Also confirm the platform actually removes the instance from the load balancer before signalling it, otherwise traffic keeps arriving at a process that has stopped accepting connections.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.