Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Express.js on Netlify: How the Function Wrapper Works, What It Cannot Do, and When to Host Express as a Real Server Instead

Sean

Platform Writer

Sep 13, 2026
9 min read

Netlify does not run an Express server. It serves static files and runs serverless functions, so to deploy Express there you wrap the app with serverless-http, export it as a function handler from netlify/functions/api.js, and add a redirect that sends /api/ to that function. That works well for a stateless JSON API. It stops working the moment the app needs a persistent connection, an in-memory session, a long-running request, a background job, or a warm database pool, because a function is created per request and thrown away. Those apps belong on a host that runs Express as a process.*

Express.js on Netlify: How the Function Wrapper Works, What It Cannot Do, and When to Host Express as a Real Server Instead

The official recipe is four commands and one file, and it is correct. What it does not say is which Express apps fit the shape it produces and which ones will spend a week being debugged into it. This post gives the recipe, then the shape, then the list of things that break, and then what to do with an app that is on the wrong side of the list.

Table of contents

The recipe

Start from an existing Express app or a fresh one. Install the wrapper and the function types, then move the app into a handler.

npm install express serverless-http @netlify/functions
mkdir -p netlify/functions
// netlify/functions/api.js
const express = require('express');
const serverless = require('serverless-http');

const api = express();
const router = express.Router();

router.get('/hello', (req, res) => res.json({ hello: 'world' }));
router.post('/items', (req, res) => res.status(201).json(req.body));

api.use(express.json());
api.use('/api/', router);

module.exports.handler = serverless(api);

Then tell Netlify that requests to /api/* belong to that function rather than to a static file:

# netlify.toml
[functions]
  directory = "netlify/functions"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/api/:splat"
  status = 200

Run netlify dev to test locally, then netlify init or a push to the connected repository to deploy. The netlify init post covers what that command writes and when to use link instead. After the deploy, /api/hello returns the JSON, and /api/items accepts a POST.

Two details that produce most of the 404s people report. The router is mounted at /api/, and the redirect rewrites /api/* to the function, so the paths must agree: a route registered at / on the router is reached at /api/. And the redirect must have status 200, which makes it a rewrite; a 301 would send the browser to the function URL and, for a POST, turn the request into a GET.

What you have built

It looks like Express, and inside a request it is Express: middleware, routers, req and res all behave. But the process model is not Express. Each request may run in a fresh instance of your function, with a fresh module scope, no memory of the previous request, a time limit, and a start-up cost paid before your first line runs. Between requests, nothing exists.

That is the right model for an API that receives a request, reads or writes a database, and answers. It scales to zero when idle and to many when busy, and you never provision anything. It is the wrong model for anything that assumes the server is a long-lived thing, which a great deal of Express code silently does.

The five things that stop working

  1. In-memory state. A session store in memory, a rate limiter that counts in a Map, a cache in a module-level variable. Each of these works in netlify dev and fails in production, because the next request lands in a different instance. Move state to a database or a store outside the function.
  2. WebSockets and server-sent events. A function answers a request and exits. It cannot hold a socket open. Real-time features need a process that stays up.
  3. Long requests. Functions have an execution limit, measured in seconds. A report generator, a bulk import, or anything that waits on a slow upstream will be killed mid-way. Background functions exist for some of this, but they are a different programming model, not an Express one.
  4. Background work. setInterval, cron-style jobs, a queue consumer that polls. There is no background in a function; when the response is sent, the instance can be frozen or destroyed. Scheduled functions cover the cron case; nothing covers a continuous worker.
  5. Database connection pools. A pool of ten connections created at module load is created again in every cold instance, and a burst of traffic can open more connections than the database allows. Use a pool size of one, a serverless-aware driver, or a connection proxy, and read the database’s connection limit before the launch rather than after.

Add a sixth that is not a failure but a cost: cold starts. The first request after a quiet period pays for the instance to start, load your modules, and connect. For a small API that is a few hundred milliseconds. For an Express app that pulls in an ORM, a validation library and a PDF renderer at import time, it can be several seconds, on every cold path.

Deciding which side of the line your app is on

Ask these before wrapping anything.

  • Does any request take longer than a few seconds? If yes, function limits will bite.
  • Does the app keep anything in memory between requests that matters? Sessions, counters, caches, a compiled template. If yes, it will misbehave under load.
  • Does it open a socket and keep it? WebSockets, SSE, a database listener. If yes, it needs a process.
  • Does it run anything on a timer or consume a queue? If yes, it needs a worker.
  • Does it need a warm connection pool to stay within a database’s limits? If yes, the function model fights the database.

No to all five, and the wrapper is a clean fit; ship it and enjoy paying nothing while it idles. Yes to any, and you are about to reimplement a server inside something designed not to be one. The Django on Netlify post reaches the same conclusion for a framework that assumes a process even harder than Express does.

Hosting Express as a real server

An Express app that needs to be a process is not hard to host; it just needs a host that runs processes. The deploy looks like this: the repository has a start script that calls node server.js, the server listens on the port from the PORT environment variable, and the platform builds the repo, runs the script, and puts a route in front of it. No handler export, no redirect rule, no wrapper.

// server.js -- the same app, listening as a process
const app = require('./app'); // your express() instance, unchanged
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`listening on ${port}`));

On RunxBuild that is a Node service from the Dev plan at $4 a month, with the build log and the runtime log on the same page, environment variables in the dashboard, a managed Postgres or MySQL beside it with a proper connection pool, and rollback to the previous deploy when one goes wrong. WebSockets stay open, timers run, the pool stays warm, and a slow request is just a slow request. The running an Express server post covers the production details, from the process manager to the health endpoint.

The two models are not rivals. A static front end on a CDN with a stateless API in functions is a fine architecture, and so is a static front end with an Express process behind it. The decision is about what the app assumes, not about which platform is better.

How this fits the rest of the stack

Wrap Express in a function when the app is stateless and quick; run it as a process when it is not. The cost of the second option is a small monthly plan rather than per-invocation billing, and it is worth seeing that number next to the database and the bandwidth before deciding. The RunxBuild hosting calculator shows the Node service, the managed database, the storage and the traffic as separate line items. Pick the model the app actually needs, then deploy it once.

Useful related references:

FAQ

Can you deploy a full Express.js server to Netlify?

Not as a persistent server. Netlify serves static files and runs serverless functions, so Express is deployed by wrapping the app with serverless-http and exporting it as a function handler. Inside a request it behaves like Express; between requests nothing persists, and anything that assumes a long-lived process will not work.

How do I set up serverless-http with Express on Netlify?

Install express, serverless-http and @netlify/functions, create netlify/functions/api.js that builds the app and exports module.exports.handler = serverless(api), and add a redirect in netlify.toml from /api/* to /.netlify/functions/api/:splat with status 200. Test with netlify dev, then deploy.

Why do I get a 404 deploying Express to Netlify?

Usually the router mount path and the redirect do not agree. If the router is mounted at /api/ and the redirect rewrites /api/* to the function, a route defined at / on the router is reached at /api/. Also confirm the redirect uses status 200, which is a rewrite, and that the functions directory in netlify.toml matches where the file lives.

Does Netlify support WebSockets or long-running Express processes?

No. A function answers one request and exits, so it cannot hold a socket open, run a timer, consume a queue, or exceed the execution time limit. Apps that need any of those should run Express as a process on a host that keeps it running.

What is the difference between Netlify Functions and a traditional Express server?

A traditional server is one process that stays up, keeps memory and connections between requests, and handles many requests concurrently. A function is created per request, with no shared memory, a time limit and a cold-start cost, and scales to zero when idle. Same Express code, different assumptions about what exists between requests.

#express js netlify#netlify express#serverless-http express#netlify functions express#deploy express app