NestJS Cold Start Timeout on AWS Lambda — Fixing the 10-Second Wall

Every NestJS team that ships to AWS Lambda hits the same wall eventually: the app is fast, tested, boringly reliable — and then the first request after any idle period takes 8 to 10 seconds, someone assumes the handler is broken, and a half-day of debugging turns up nothing wrong with a single line of business logic. That's the NestJS cold start timeout on AWS Lambda, and it isn't a bug. It's NestJS's dependency injection container doing, in full, on your user's dime, the exact bootstrap work it was designed to do once and reuse forever.
Short answer: NestJS's DI container has to instantiate every provider and resolve every module's dependency graph before your handler can run, and on a cold Lambda instance there's no warm process to skip that cost. Fix it by trimming what actually runs at bootstrap (lazy-load rarely-hit modules, skip Swagger/validation setup you don't need per-invocation), wrapping the app correctly with an adapter like @vendia/serverless-express so you're not re-bootstrapping per request, and reaching for provisioned concurrency only after you've done both — not instead of them.

Why NestJS Cold Start Timeout on AWS Lambda Happens at All
A plain Express handler cold-starts in well under a second because there's almost nothing to set up — Express itself is a thin router. NestJS is a different animal by design: @Injectable() providers, @Module() boundaries, guards, interceptors, and pipes all get wired together by the DI container at bootstrap, and that container has to walk the entire dependency graph before your handler() can accept its first request. On a warm Lambda instance, that graph gets built once and the container stays in memory across thousands of invocations — the cost is invisible. On a cold one, whoever sends the first request eats the entire bootstrap bill personally, and if you're loading an ORM, Swagger, class-validator metadata, and six feature modules eagerly, that bill runs 8-10 seconds.
(If you've been staring at your handler function trying to find the slow line — stop. The slow part isn't in your handler. It's in everything that runs before your handler is even reachable, and that part is basically invisible unless you go looking for it with a timer around NestFactory.create().)
Lazy-Loading Modules vs. Eager Bootstrap: Where the Time Actually Goes
The default NestJS bootstrap is eager: every module you import gets instantiated at startup, whether the current invocation needs it or not. A webhook handler that only ever touches your NotificationsModule still pays to instantiate BillingModule, ReportingModule, and anything else sitting in your root AppModule imports. NestJS's LazyModuleLoader exists specifically for this — it lets you defer a module's instantiation until the first request that actually needs it, instead of paying for all of them on every cold start.
1// notifications.controller.ts
2import { Controller, Post, Body } from '@nestjs/common';
3import { LazyModuleLoader } from '@nestjs/core';
4
5@Controller('webhooks')
6export class WebhooksController {
7 constructor(private readonly lazyModuleLoader: LazyModuleLoader) {}
8
9 @Post('stripe')
10 async handleStripeWebhook(@Body() payload: unknown) {
11 // BillingModule only instantiates on the invocations that hit this route
12 const { BillingModule } = await import('./billing/billing.module');
13 const moduleRef = await this.lazyModuleLoader.load(() => BillingModule);
14 const { BillingService } = await import('./billing/billing.service');
15 const billingService = moduleRef.get(BillingService);
16 return billingService.processWebhook(payload);
17 }
18}This is worth doing before you reach for anything else, because it directly attacks the thing that's actually slow — the size of the dependency graph the container has to resolve — rather than paying AWS to hide the problem.

Wrapping NestJS Correctly for Lambda: @vendia/serverless-express
The second common mistake is subtler: re-running NestFactory.create() on every single invocation instead of caching the bootstrapped app across warm invocations. @vendia/serverless-express (the maintained successor to aws-serverless-express) handles this correctly — bootstrap once at module load time, then reuse the same app instance for every request the warm container serves.
1// lambda.ts
2import { NestFactory } from '@nestjs/core';
3import serverlessExpress from '@vendia/serverless-express';
4import { Handler } from 'aws-lambda';
5import { AppModule } from './app.module';
6
7let cachedServer: Handler;
8
9async function bootstrap(): Promise<Handler> {
10 const app = await NestFactory.create(AppModule);
11 await app.init();
12 const expressApp = app.getHttpAdapter().getInstance();
13 return serverlessExpress({ app: expressApp });
14}
15
16export const handler: Handler = async (event, context, callback) => {
17 // Only re-bootstraps on a genuinely cold container
18 cachedServer = cachedServer ?? (await bootstrap());
19 return cachedServer(event, context, callback);
20};If you're deploying this in a container image rather than a zip, the same "trim what runs at boot" logic that shrinks a multi-stage Docker build applies here too — a smaller, leaner image pulls faster, and a leaner bootstrap runs faster. Same principle, two different bills.
Provisioned Concurrency: A Real Stopgap, Not a Fix
AWS's provisioned concurrency keeps a set number of instances permanently warm, which sidesteps cold starts entirely for the traffic it covers. It genuinely works. It's also a recurring line item you pay for every environment — staging, prod, every long-lived branch preview — for as long as the app stays slow to boot. Cloud teams already say managing spend is their single biggest cloud challenge, with roughly 29% of spend going to waste (Flexera, 2024), and unaudited provisioned concurrency left running on a staging environment nobody remembers to scale down is exactly the kind of line item that stat is describing. Trim the bootstrap first. Add provisioned concurrency second, sized to the traffic pattern that actually needs it — not as a substitute for the engineering work.
The Opinion Part: You Might Not Need Lambda for This At All
Here's the position I'll actually stand behind: if you're running a SaaS product with steady, predictable traffic and you're spending afternoons trimming a NestJS bootstrap to shave Lambda cold starts, you've probably reached for serverless before the traffic pattern asked for it. Lambda's economics make sense for genuinely bursty, infrequent workloads — an internal tool, a low-volume webhook receiver. For a product with steady request volume, an always-on container on Fargate, Railway, or a small VM sidesteps this entire class of problem, often at a comparable or lower bill than a warm Lambda fleet. You are not required to solve a cold-start problem you could avoid having in the first place by matching the hosting model to the traffic you actually have — not the traffic a serverless conference talk convinced you to plan for.
I shipped a 2GB Docker image to production once, on an always-on host, and it was slow for an entirely different reason (the health check timed out before the container even finished pulling). A multi-stage build got it to 180MB. The lesson generalizes further than Docker: whatever's making your startup slow — a bloated image or a bloated DI graph — the fix is almost always "load less at boot," not "pay a platform to hide how much you're loading."

Trimming the Bootstrap: What to Actually Cut
Beyond lazy module loading, a few concrete things routinely bloat a Lambda-bound NestJS bootstrap for no benefit at invocation time:
- Swagger/OpenAPI setup (
SwaggerModule.setup(...)) — useful for local dev and staging docs, dead weight on every production cold start. Gate it behind an environment check. - Global validation pipes with
whitelist/transformon every route — necessary, but make sure you're not double-instantiating validator metadata per module unnecessarily. - Eager database connections at bootstrap — if your ORM connects during
onModuleInit, that connection setup is now part of your cold-start tax, not a background concern. Consider deferring non-critical connections. - Anything logging verbosely during bootstrap — this doesn't cost much CPU, but it's a signal something is doing more work at boot than it needs to.
If you're chasing a lingering process-level slowdown that isn't cold-start-related — the app is fine on the first request but degrades over hours — that's a different problem with a different fix, and our Node.js memory leak debugging guide covers the heap-snapshot approach for that specifically.
Conclusion
The NestJS cold start timeout on AWS Lambda isn't a mystery once you know where to look: it's the DI container doing real, necessary work with no warm process to amortize it over. Lazy-load what you can, wrap the app correctly so you're not re-bootstrapping every invocation, and treat provisioned concurrency as a bill you're choosing to pay rather than a fix you've applied. And if you're two hours into trimming modules and starting to wonder whether Lambda was the right call for a product with steady traffic — that's a legitimate question, not a distraction, and it's worth answering honestly before you optimize your way around an architecture decision you didn't have to make.
Frequently Asked Questions
NestJS's dependency injection container has to instantiate every provider, resolve every module's dependency graph, and run every registered lifecycle hook before it can handle a single request. On a warm Lambda instance that cost is paid once and amortized over thousands of requests. On a cold one, your very first user pays the entire bootstrap cost themselves, which is why it shows up as an 8-10 second delay instead of a rounding error.
It depends heavily on module count and what runs in bootstrap, but 3-6 seconds is common for a mid-sized app, and 8-10 seconds is not unusual once you're loading TypeORM/Prisma, validation pipes, Swagger, and a handful of feature modules eagerly. Compare that to a bare Express handler with no DI container, which typically cold-starts in well under a second.
It solves the symptom, not the cause — you're paying AWS to keep instances warm instead of fixing why bootstrap is slow. It works, and it's a legitimate stopgap, but it's a recurring bill for every environment (staging, prod, every branch preview) rather than a one-time engineering fix. Trim the bootstrap first; add provisioned concurrency only for the traffic pattern that still needs it after that.
If your traffic is genuinely bursty and infrequent — an internal tool, a low-volume webhook receiver — serverless economics make sense and it's worth fixing the cold start. If you're running a SaaS product with steady, predictable traffic, an always-on container on Fargate, Railway, or a small VM sidesteps this entire problem class for a comparable or lower bill. Don't debug your way around an architecture mismatch you could sidestep instead.
A minimal framework with no DI container — plain Express, Fastify without Nest's abstraction layer, or raw Lambda handlers — cold-starts noticeably faster because there's no dependency graph to resolve at boot. That's a real tradeoff: you give up NestJS's structure and testability for Lambda-native speed. For most teams, trimming the NestJS bootstrap is a better trade than rewriting the app in a thinner framework.
