How We Reduced Our Node.js Cold Starts by 90%
Our Node.js APIs were fast.
How We Reduced Our Node.js Cold Starts by 90%
Our Node.js APIs were fast.
Until they weren’t.
Everything looked fine during steady traffic, but after a few minutes of inactivity, the next request would suddenly take 2–3 seconds. **non members can read here**
The problem wasn’t the database.
It wasn’t the network either.
It was cold starts.

And after profiling startup traces and analyzing dependency graphs, we realized something important:
Most cold start problems are architecture problems disguised as infrastructure problems.
After simplifying our runtime, reducing dependencies, and changing how the app initialized, we reduced cold starts by nearly 90%.
Here’s exactly what worked.
What Is a Cold Start?
A cold start happens when your application boots from scratch before serving a request.
This usually happens in:
- serverless functions
- autoscaled containers
- edge runtimes
- suspended microservices
Before handling the first request, Node.js must:
- initialize the runtime
- load dependencies
- parse modules
- initialize frameworks
- connect databases
- execute startup logic
If the application does too much work during startup, users immediately feel the delay.
Our Original Setup
The original API stack looked pretty standard:
express
axios
dotenv
mongoose
lodash
moment
winston
aws-sdk
The issue wasn’t one package.
It was the combined startup cost of everything together.
At startup, the application was:
- loading hundreds of modules
- initializing middleware chains
- connecting databases immediately
- importing SDKs globally
- loading telemetry services
- parsing huge dependency trees
The runtime spent more time preparing the app than serving requests.
1. We Replaced Axios With Native Fetch
This was one of the easiest wins.
Before:
import axios from 'axios';
const { data } = await axios.get(
'https://api.example.com/users'
);
After:
const response = await fetch(
'https://api.example.com/users'
);
const data = await response.json();
Modern Node.js already ships with native fetch.
Removing Axios helped us:
- reduce dependencies
- reduce bundle size
- reduce module parsing time
For high-scale APIs, even small startup savings compound quickly.
2. We Removed Large Utility Libraries
We were importing Lodash for tiny operations.
Before:
import _ from 'lodash';
const grouped = _.groupBy(users, 'role');
After:
const grouped = Object.groupBy(
users,
user => user.role
);
Another example:
Before:
import moment from 'moment';
const formatted = moment().format('YYYY-MM-DD');
After:
const formatted = new Intl.DateTimeFormat(
'en-CA'
).format(new Date());
Modern JavaScript has evolved dramatically.
A lot of utility packages are no longer necessary.
3. We Replaced Express for Small APIs
Some of our APIs only had 2–3 endpoints.
Using a full framework stack added unnecessary startup overhead.
Before:
import express from 'express';
const app = express();
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000);
After:
import http from 'node:http';
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.setHeader(
'Content-Type',
'application/json'
);
res.end(JSON.stringify({
status: 'ok'
}));
}
});
server.listen(3000);
For small APIs, native Node.js was enough.
This reduced:
- framework initialization
- middleware loading
- routing overhead
4. We Lazy Loaded Expensive SDKs
This was a huge improvement.
Our AWS SDK initialization was happening during startup even when requests never used S3.
Before:
import AWS from 'aws-sdk';
const s3 = new AWS.S3();
That code executed during every cold start.
Instead, we lazy loaded the SDK only when required.
After:
let s3;
async function uploadFile(file) {
if (!s3) {
const AWS = await import('aws-sdk');
s3 = new AWS.S3();
}
return s3.upload(file).promise();
}
Cold starts became dramatically faster because heavy SDKs stopped blocking startup.
5. We Delayed Database Connections
Originally, MongoDB initialized immediately:
await mongoose.connect(process.env.DB_URL);
That meant every cold start waited for database negotiation before serving requests.
Instead, we switched to lazy connection management.
let connection;
export async function getDb() {
if (!connection) {
connection = await mongoose.connect(
process.env.DB_URL
);
}
return connection;
}
Now the API boots instantly and connects only when needed.
6. We Split Large Modules
One large controller file was importing everything globally:
import analytics from './analytics.js';
import reports from './reports.js';
import billing from './billing.js';
import ai from './ai.js';
Even routes that didn’t use those features still paid the startup cost.
We split features into smaller modules.
if (req.url.startsWith('/ai')) {
const ai = await import('./ai.js');
return ai.handle(req, res);
}
This significantly reduced the critical startup path.
7. We Reduced Docker Image Size
Our original container image was huge.
Before:
FROM node:latest
After:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]
Smaller images improved:
- deployment speed
- container startup
- autoscaling performance
8. We Stopped Loading Everything at Startup
This was one of the biggest hidden problems.
The app initialized:
- telemetry
- analytics
- schedulers
- cache warmers
- feature flags
- background jobs
during every startup.
Most requests didn’t even use those systems.
We moved non-critical services into asynchronous background initialization.
Before:
await initTelemetry();
await initAnalytics();
await warmCache();
After:
setImmediate(async () => {
await initTelemetry();
await initAnalytics();
await warmCache();
});
The API became responsive much faster.
The Results
After simplifying the architecture:

The infrastructure never changed.
The architecture did.
The Biggest Lesson
We often blame cloud providers for slow cold starts.
But in many cases, the real issue is application complexity.
Modern Node.js is already extremely fast.
What slows applications down is usually:
- excessive abstractions
- oversized dependency trees
- unnecessary initialization
- heavy frameworks
- too much startup work
The fastest Node.js application is usually the one doing the least work before handling requests.
Final Thoughts
Frameworks still make sense for large systems.
But for:
- small APIs
- serverless functions
- edge workloads
- internal tools
- lightweight microservices
modern Node.js can often do far more natively than developers realize.
Before adding another dependency or framework layer, ask yourself:
“Does this improve the application, or just increase startup cost?”
Sometimes the best optimization is simply removing things.
메타데이터
- post_id
- 145c73db7d9c
- slug
- how-we-reduced-our-node-js-cold-starts-by-90-145c73db7d9c
- url
- https://medium.com/front-end-world/how-we-reduced-our-node-js-cold-starts-by-90-145c73db7d9c
- canonical_url
- https://medium.com/front-end-world/how-we-reduced-our-node-js-cold-starts-by-90-145c73db7d9c
- author_url
- https://medium.com/@sachinkasana
- status
- ok
- fetched_at
- 2026-06-12 22:02:08