Serverless Deployment
NestJS is built on top of Node HTTP frameworks, which makes it a natural fit for serverless runtimes like AWS Lambda, Azure Functions, or Google Cloud Functions. The trick is to stop listening on a port and instead translate each cloud event into a request the Nest application can answer. The two challenges that dominate serverless deployments are wrapping the app correctly and keeping cold starts short, and this page walks through both.
How serverless changes the runtime model
In a traditional deployment you call app.listen(3000) and the process stays alive, handling many requests over a long-lived socket. On Lambda there is no port: the platform invokes an exported handler function once per request (or per batch of records), passing an event and a context. Your job is to bridge that event-driven contract to Nest’s request/response pipeline.
The standard bridge is serverless-http, which converts an API Gateway / Lambda event into a Node-compatible request and serializes the response back into the shape the platform expects.
Never call
app.listen()in a Lambda handler. Listening on a port keeps the invocation hanging until it times out. Useapp.init()instead so the DI container, middleware, and lifecycle hooks are wired without opening a socket.
Wrapping the app with serverless-http
Install the adapter alongside the Express types Nest already uses:
npm install serverless-http
npm install -D @types/aws-lambda
The key idea is to bootstrap Nest once and cache the resulting handler in a module-level variable. Because Lambda reuses a warm execution context across invocations, this cache means only the first request pays the bootstrap cost.
// src/lambda.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from 'serverless-http';
import express from 'express';
import type { Handler, APIGatewayProxyEvent, Context } from 'aws-lambda';
import { AppModule } from './app.module';
let cachedHandler: Handler;
async function bootstrap(): Promise<Handler> {
const expressApp = express();
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressApp),
{ logger: ['error', 'warn'] },
);
app.enableCors();
// Apply global pipes/filters here exactly as you would in main.ts.
await app.init(); // NOT app.listen()
return serverlessExpress(expressApp);
}
export const handler: Handler = async (
event: APIGatewayProxyEvent,
context: Context,
) => {
context.callbackWaitsForEmptyEventLoop = false;
cachedHandler = cachedHandler ?? (await bootstrap());
return cachedHandler(event, context);
};
Setting callbackWaitsForEmptyEventLoop = false lets the function return as soon as the response is ready, even if open database pools or timers remain in the loop — without it, Lambda waits and you pay for idle billing time.
Deploying with the Serverless Framework
A minimal serverless.yml points the platform at the compiled handler and routes every path through a single proxy integration:
service: nestjs-api
provider:
name: aws
runtime: nodejs20.x
memorySize: 1024
timeout: 15
functions:
api:
handler: dist/lambda.handler
events:
- httpApi:
path: /{proxy+}
method: any
Build and deploy:
npm run build
npx serverless deploy
Output:
Deploying nestjs-api to stage dev (us-east-1)
✔ Service deployed to stack nestjs-api-dev (74s)
endpoint: ANY - https://abc123.execute-api.us-east-1.amazonaws.com/{proxy+}
functions:
api: nestjs-api-dev-api (3.1 MB)
Taming cold starts
A cold start is the time spent initializing a fresh execution environment: loading the bundle, parsing modules, and running your Nest bootstrap. The levers that matter most:
| Technique | Effect | Trade-off |
|---|---|---|
| Cache the handler at module scope | Skips bootstrap on warm invocations | None — always do this |
| Bundle and tree-shake (esbuild/webpack) | Less code to load and parse | Build step complexity |
Increase memorySize | More CPU, faster init | Higher per-ms cost |
| Lazy-load heavy modules | Smaller initial graph | Pay cost on first use |
| Provisioned concurrency | Eliminates cold starts | Fixed monthly cost |
Bundling has the biggest payoff. The default Nest build ships thousands of files; bundling collapses them into one. With the Nest CLI and the SWC/webpack option:
npm install -D @nestjs/cli
// nest-cli.json
{
"compilerOptions": {
"webpack": true,
"webpackConfigPath": "webpack.config.js"
}
}
Avoid pulling in modules whose initialization is expensive (large config validators, ORM metadata scanning) at the top of
AppModuleif only a few routes need them. Defer them withLazyModuleLoaderso the cold start does not pay for code a given request never touches.
Reusing connections across invocations
Database and HTTP clients should be created once and reused. Because the cached handler keeps the Nest container alive, a connection pool opened during app.init() persists across warm invocations automatically — provided you do not tear it down. Do not register a graceful-shutdown hook that closes pools on every request; let the platform recycle the environment instead.
// Keep the pool small — Lambda runs one request per container at a time.
TypeOrmModule.forRoot({
type: 'postgres',
url: process.env.DATABASE_URL,
extra: { max: 2 },
});
Best Practices
- Bootstrap once and cache the handler at module scope so warm invocations skip initialization entirely.
- Call
app.init(), neverapp.listen(), inside the Lambda handler. - Set
context.callbackWaitsForEmptyEventLoop = falseto avoid paying for idle event-loop time. - Bundle the app with webpack or esbuild to shrink the package and slash parse time.
- Keep connection pools tiny (1-2 connections) since each container serves one concurrent request.
- Use provisioned concurrency for latency-sensitive endpoints where cold starts are unacceptable.
- Trim the logger and disable unnecessary CORS/Swagger setup in the serverless path to keep bootstrap lean.