Optimizing Cold Starts in a Node.js Lambda: What the Docs Don’t Tell You
If you’ve ever worked on AWS Lambda, you will know that one word that buzzes every developer is – cold start.
It’s like Thanos, an inevitable tax that you pay when Lambda spins up a fresh execution environment to handle a request.
In Node.js, while the AWS documentation covers the basics (provisioned concurrency, minimizing package size, etc.) of survival methods, there are real-world nuances that rarely make it into the documentation. These nuances can make a huge difference.
This blog is less about rehashing the documentation and more about the battle-tested insights you only gain after troubleshooting production workloads.
The Cold Start Reality Check
A Node.js AWS Lambda cold start actually breaks down into three clear milestones:
- Container spin-up in which AWS fires up a fresh instance with all mandated compute and memory.
- Code initialization is when Node loads all the dependencies, environmental variables, and source code.
- Handler execution is where the real work begins.
The first two phases are where most of the latency lies. And this is exactly where you, as an architect, have leverage.
What the Docs Tell You (and Why It’s Not Enough)
The AWS documentation confronts you with three perfectly reasonable tips:-
- Trim the bundle size.
- Use provisioned concurrency in production.
- Keep functions warm with scheduled invocations.
Each step is valid, but none is comprehensive. Provisioned concurrency is expensive. Scheduled warmers break down if scaled. Bundle trimming rarely makes a change.
What are the invisible rules for improving Node.js Cold Starts?
1. Stop including initialization as a free.
Every dependency loaded, every database connection established contributes to the bill of cold start.
Bad practice: Connecting to RDS or initializing a heavy ORM (like Sequelize/TypeORM) at the top level.
Better approach: Lazy load connections within the handler or connection manager, cache them in the container, and reuse.
let cachedDb = null; async function getDb() { if (!cachedDb) { cachedDb = await createConnection(); // happens once per container } return cachedDb; }; exports.handler = async (event) => { const db = await getDb(); return db.query("SELECT now()"); };
This way, the penalty is paid once per container, not per cold start.
2. Pay Attention to the Dependency Graph
- The node_modules directory is probably to blame for increasing cold starts more than your source code. Meta dependencies like aws-sdk are in the Lambda runtime anyway, why bundle them? Libraries that utilize reflection or polyfills can increase the init time significantly.
- For example, I went from moment.js to date-fns in one of my projects, and saved 400ms.
Pro-tip: Use webpack or esbuild with tree-shaking in order to remove dead code. What matters is the number of modules Node has to resolve at init, not just the absolute size.
3. Rethink Logging
It may sound trivial but logging libraries often hide expensive initialization behind the scenes. A JSON structured logger instantiated with synchronous file streams can delay the init by more than 100 milliseconds.
- Choose async loggers (the most minimal transports) like pino or winston.
- Don’t configure transports (CloudWatch or S3) until it is actually needed – do so lazily.
4. Memory ≠ Memory
Adding memory to a Lambda function means not just granting more RAM, but also comparatively more CPU. For Node.js, these environments will result in faster init times.
- A function at 512 MB can have a 40-60% faster cold start than a function at 128 MB.
- In some cases, doubling the memory actually reduces the total runtime cost because the execution time has decreased significantly.
5. Bundle Native Code with Care
Native modules (bcrypt, sharp, etc.) can unfortunately add a terrible cold start for the user because of binary loading. So if you have to use them:
- Precompile them for the Lambda runtime.
- Use less resource-heavy alternatives (argon2-browser, jimp)
6. Play the Long Game with Provisioned Concurrency
Docs say “turn it on, problem solved.” In reality, it’s a balancing act:
- Overprovision: You’re burning money.
- Underprovision: You’re still hitting cold starts.
The trick is to align concurrency scaling with actual traffic patterns. Tools like Application Auto Scaling and predictive scaling can help, but the real win is observability—measure cold start frequency per function, then apply provisioned concurrency only where it’s financially justified.
Observability: The Last Piece to this puzzle
One of the reasons engineers struggle with cold starts is that they don’t observe them properly. Cold start latency is often buried at the bottom of the Init Duration in CloudWatch logs.
Do not just measure averages, measure:
- P95 and P99 cold start times
- Cold start frequency vs. warm start frequency
- Memory vs latency correlation
Without these details, we are truly flying blind and likely overspending on provisioned concurrency.
Conclusion
Cold starts in Node.js Lambda have not been solved and are very much going to be a part of the foreseeable future. The difference between a slower function and a faster one is not necessarily going to be about following the documentation word for word. It will be about treating initialization as a first-class citizen in your architecture.
For technical leads, here is the takeaway:
- Treat your dependency graph (just as you would for auditing your infra costs)
- Defer as much as possible until it is truly needed.
- Technologies for observability should serve decisions, not assumptions
A 2-second cold start doesn’t just hurt latency, It chips away at the developer confidence in serverless as a viable production platform.