NodeJSServerless Node.js

Serverless Node.js

Serverless means running code without managing a server — you deploy a function, and the platform (AWS Lambda, GCP Cloud Functions, Vercel Edge, Cloudflare Workers) runs it on demand, scales it to zero when idle, and charges per invocation. For the right workload — event-driven processing, spiky traffic, webhooks, API routes — it removes almost all operational overhead. For the wrong workload — long-running processes, stateful connections, cold-start-sensitive paths — it's a poor fit. This page covers how serverless Node works, the event model, adapting an Express app, cold starts and how to mitigate them, and the execution constraints you must design around.

How serverless Node works

A basic Lambda handler

JS
// A serverless function is a MODULE EXPORT, not a long-running server.
// The platform calls it on each event and tears it down when idle.
export const handler = async (event, context) => {
  // 'event' varies by trigger:  API Gateway request, S3 event, SQS message, timer...
  const name = event.queryStringParameters?.name ?? 'world'
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message: `Hello, ${name}!` }),
  }
}
A serverless function is an exported handler, not a server — the platform invokes it per event and recycles the container between calls
The fundamental shift is that serverless code doesn't `app.listen()`. Instead you **export a handler function** that the platform calls with an **event** (the trigger payload) and a **context** (metadata about the invocation). The platform manages the container lifecycle: it spins up a container to handle a request, keeps it **warm** for a period to avoid cold-starting the next one, and eventually recycles it. Between invocations the container may be frozen or discarded. This is why stateful in-memory patterns break — the container you wrote to may not be the one the next request hits. But it means you pay per invocation, the platform handles all scaling automatically (from 0 to thousands of concurrent executions), and there are no servers to patch, no process managers to configure, no load balancers to provision.
Adapting Express for serverless

JS
// Option A: use an adapter that wraps your Express app as a Lambda handler.
import serverlessExpress from '@vendia/serverless-express'
import app from './app'           // your normal Express app (no .listen())

export const handler = serverlessExpress({ app })

// Option B: serverless frameworks (SST, Architect) handle routing natively.
// Option C: use a framework designed for serverless (Next.js API routes, Hono).
Wrap an existing Express app with an adapter, or build with a serverless-native framework — don't call `.listen()` in a Lambda
You can run an existing [Express](/nodejs/express-intro) app on Lambda using an **adapter** like `@vendia/serverless-express` or `serverless-http` — it wraps your app and translates between Lambda events and Express's HTTP request/response format, so your routes, middleware, and error handling work unchanged. This is the quickest migration path. Alternatively, build the API using a **serverless-native router** like Hono or tRPC (lighter cold starts than a full Express server), or a higher-level framework like SST that manages the infrastructure alongside your code. Either way, the key constraint is no `.listen()` — the function package is a module that exports a handler; the platform is the server.
Cold starts — the main serverless gotcha

Text
Cold start sequence (only when the container is recycled):
   Platform allocates a container
   → downloads your deployment package
   → starts Node runtime
   → executes module-level code (requires, DB connections, SDK clients)
   → runs your handler function
   ─────────────────────────────────────── can take 200ms–2s+ for Node

Warm invocation (container already running):
   → runs your handler function                  ← typically 1–10ms overhead
Cold starts add latency on first invocations — keep the bundle small, init connections lazily, and use provisioned concurrency for latency-sensitive paths
A **cold start** happens when the platform can't reuse a warm container — on first deploy, after scaling out, or after a period of no traffic. For Node, the cold start includes starting the runtime and executing all module-level code: requiring dependencies, initializing DB connection pools, loading configuration. A bloated bundle with heavy dependencies (AWS SDK, ORM, everything) can push cold starts above 1–2 seconds — unacceptable for user-facing routes. Mitigations: **keep the bundle small** (tree-shake, import only what you use, `aws-sdk` v3 modular imports); **initialize connections lazily** (establish the DB pool on the first invocation, not at module load — the connection is then reused by the warm container); use **provisioned concurrency** (AWS/GCP keep pre-warmed instances ready at a cost) for latency-sensitive endpoints. Cold starts matter most for interactive API routes and least for background processors like S3 triggers or SQS consumers.
Execution constraints to design around

Constraint

Implication

Timeout (e.g. 15 min max on Lambda)

No long-running processes; offload to a queue/background job

Ephemeral filesystem (/tmp only, lost between invocations)

Don't write permanent files; use S3/storage

No persistent connections between invocations

Use connection pooling proxies (RDS Proxy, PgBouncer) for databases

Stateless / no shared memory

All shared state in external services (Redis, DB)

Concurrency = copies, not threads

High concurrency = many parallel cold Lambda containers

Serverless functions are short-lived, stateless, and may run as hundreds of parallel copies — design every dependency accordingly
Serverless execution has hard constraints that force certain design choices. The **timeout** (Lambda maxes at 15 minutes) means no long-running work — heavy processing goes through a queue consumed by a longer-running service or a Step Functions workflow. The **ephemeral filesystem** means anything written to `/tmp` is per-invocation and discarded — use S3 for real file output. **Database connections** are a classic pitfall: a new Lambda container opens a new connection, and at 1000 concurrent invocations that's 1000 simultaneous connections to Postgres (which has a limit of a few hundred) — the solution is a **connection pooling proxy** (RDS Proxy or PgBouncer) that multiplexes many Lambda connections to a small pool. Design with these constraints in mind from the start; retrofitting a traditionally-architectured app into serverless is where teams hit the most friction.
Next
Automate the build-test-deploy pipeline: [CI/CD Pipelines](/nodejs/ci-cd).