Skip to content
Node.js nd deployment 5 min read

Serverless Node.js (AWS Lambda)

Serverless lets you deploy individual Node.js functions that the cloud provider runs on demand, scaling from zero to thousands of concurrent executions without you managing a single server. You write a handler, the platform receives an event (an HTTP request, a queue message, a scheduled timer) and invokes your code, then bills you only for the milliseconds it ran. This page covers writing AWS Lambda handlers, taming cold starts, deploying with the Serverless Framework, reusing connections across invocations, and the trade-offs against an always-on server.

Writing a Lambda handler

A Lambda handler is an exported async function that receives an event object and a context object and returns a response. The shape of the event depends on what triggered the function; the value you return becomes the integration’s response. AWS Lambda has supported ES modules since the nodejs18.x runtime, so you can use export directly as long as your file ends in .mjs or your package.json declares "type": "module".

// handler.mjs — runtime: nodejs22.x
export const handler = async (event, context) => {
  const name = event.queryStringParameters?.name ?? "world";

  return {
    statusCode: 200,
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ message: `Hello, ${name}!`, requestId: context.awsRequestId }),
  };
};

Invoking this behind an API Gateway HTTP API with ?name=Ada returns:

Output:

{"message":"Hello, Ada!","requestId":"a1b2c3d4-5e6f-7890-abcd-ef1234567890"}

Never call process.exit() or leave the event loop blocked — Lambda freezes the execution environment after the response and thaws it for the next invocation. Returning (or throwing) is how you signal completion.

Cold starts

When no warm execution environment is available, Lambda must allocate one, download your code, start the Node.js runtime, and run any top-level module code before your handler executes. That first-time latency is the cold start. Subsequent invocations reuse the warm environment and skip all of it, which is why the second request to a function is dramatically faster than the first.

Node.js has one of the lowest cold-start penalties of the major runtimes, but bundle size and top-level work still matter. Keep these factors in mind:

FactorEffect on cold startMitigation
Deployment package sizeLarger zip takes longer to fetch and unpackBundle and tree-shake with esbuild
Top-level importsEvery import runs at init timeImport only what the handler uses; lazy-load heavy SDKs
Memory settingMore memory also grants more CPURaise memory to speed init and execution
VPC attachmentAdds ENI setup latencyAvoid VPC unless you need private resources

Provisioned Concurrency keeps a configured number of environments pre-warmed so latency-sensitive endpoints never pay a cold start — but you pay for that reserved capacity around the clock, which erodes the pay-per-use benefit.

Reusing connections across invocations

Because Lambda freezes and reuses the environment, anything you declare outside the handler survives between invocations on the same warm instance. This is the single most important performance pattern in serverless Node: create expensive clients (database pools, AWS SDK clients, HTTP keep-alive agents) once at module scope, not inside the handler.

// db.mjs
import { Pool } from "pg";

// Created once per execution environment, reused across invocations.
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 1, // one connection per warm Lambda; let RDS Proxy multiplex
  idleTimeoutMillis: 30_000,
});

export const handler = async (event) => {
  const { rows } = await pool.query("SELECT id, email FROM users WHERE id = $1", [event.userId]);
  return { statusCode: 200, body: JSON.stringify(rows[0] ?? null) };
};

Set context.callbackWaitsForEmptyEventLoop = false if you keep open handles (like a pool) and use the older callback style, otherwise Lambda waits for the event loop to drain before responding. With many concurrent Lambdas each holding connections, a traditional database can exhaust its connection limit — front it with Amazon RDS Proxy or a serverless-native database to pool connections safely.

Deploying with the Serverless Framework

The Serverless Framework turns a declarative serverless.yml into CloudFormation, provisioning the function, its triggers, IAM role, and log groups in one command. It removes the boilerplate of wiring API Gateway and Lambda together by hand.

# serverless.yml
service: greeting-api
provider:
  name: aws
  runtime: nodejs22.x
  region: us-east-1
  memorySize: 512
  environment:
    DATABASE_URL: ${env:DATABASE_URL}

functions:
  greet:
    handler: handler.handler
    events:
      - httpApi:
          path: /greet
          method: get

Install the CLI and deploy:

npm install --save-dev serverless serverless-esbuild
npx serverless deploy

Output:

Deploying greeting-api to stage dev (us-east-1)

✔ Service deployed to stack greeting-api-dev (84s)

endpoint: GET - https://abc123.execute-api.us-east-1.amazonaws.com/greet
functions:
  greet: greeting-api-dev-greet (1.1 MB)

The serverless-esbuild plugin bundles each function into a minimal artifact, which directly shrinks cold starts. Use npx serverless invoke -f greet to test a deployed function and npx serverless remove to tear the whole stack down.

Trade-offs vs always-on servers

Serverless is not universally better than a long-running Express or Fastify server; it is a different cost and operational model. Choosing well means matching the model to your traffic shape.

ConcernServerless (Lambda)Always-on server
ScalingAutomatic, per-request, to zeroManual or autoscaling groups
Cost at low trafficNear zero (pay per use)Pays for idle capacity
Cost at sustained high trafficCan exceed a reserved instanceOften cheaper per request
LatencyCold starts on scale-upConsistently warm
Long-lived connectionsHard (WebSockets need extra services)Native
Execution limit15-minute max per invocationUnbounded
Local developmentNeeds emulationJust run the process

Lambda shines for spiky, event-driven, or low-baseline workloads. A steady, high-throughput API or anything needing persistent connections, sub-millisecond latency, or runs longer than 15 minutes is usually better and cheaper on a containerized always-on server.

Best Practices

  • Initialize clients, pools, and SDK objects at module scope so warm invocations reuse them instead of reconnecting.
  • Bundle handlers with esbuild and import lazily to keep deployment packages small and cold starts low.
  • Front relational databases with RDS Proxy (or use a serverless database) to avoid exhausting connection limits under concurrency.
  • Grant each function a least-privilege IAM role rather than sharing one broad role across the service.
  • Pin the runtime (nodejs22.x) and set a sensible memorySize — more memory buys more CPU and faster execution.
  • Use Provisioned Concurrency only for latency-critical endpoints where the steady cost is justified.
  • Set structured logging and a timeout slightly above your expected p99 so runaway invocations fail fast instead of burning budget.
Last updated June 14, 2026
Was this helpful?