AWS Lambda Cold Starts: What Actually Causes Them, and What You Can Do

AWS Lambda Cold Starts: What Actually Causes Them, and What You Can Do

Arthur

Cold starts. The two-word ghost story we tell each other at AWS meetups. Let's actually look inside the box.

What "cold start" actually means

When your Lambda gets invoked, AWS needs an execution environment — a Firecracker micro-VM with the runtime, your code, layers, and configured memory ready to go. If there's one sitting around idle from a previous invocation, you reuse it (warm path: ~1ms overhead). If not, AWS has to build one. That's the cold start.

Roughly, here's what happens in order:

  1. Resource scheduling. AWS allocates a slot in the right AZ with the right CPU/memory class.

  2. Firecracker VM boot. The micro-VM spins up. Fast — usually 100–200ms.

  3. Runtime init. Node, Python, the JVM, .NET. JVM is the famous offender (1–3s); Node and Python are typically 200–500ms.

  4. Code download & init. Your zip is pulled from S3 and unpacked. Top-of-file code runs (imports, SDK clients, connection pools).

  5. Handler invocation. Only now does handler(event, context) fire.

Total: anywhere from ~300ms (lean Node) to 8+ seconds (fat Java with a Spring container). And the thing nobody tells you upfront — memory size affects all of this proportionally. A 128MB function and a 1769MB function have wildly different cold-start budgets before your code even runs.

What's actually slow (and what isn't)

People fixate on the wrong stage. From production data across services we've worked on, the time-spent breakdown for a typical cold start looks roughly like:

  • VM provisioning: ~150ms (you can't change this)

  • Runtime boot: 200ms–3s (depends on language)

  • Code download: 50–500ms (depends on package size)

  • Init code: 200ms–5s (depends on what you put there)

That last one is where almost all the avoidable pain lives. SDK clients that connect on construction, ORM bootstrap, framework reflection scans, secret loading from Parameter Store — all of it runs sequentially during init, all of it bills you for cold-start latency.

What actually helps (in order of impact)

1. Move connection pooling out of the hot path

If you're hitting RDS or any TCP database, a fresh connection on every cold start is murderous. Two options:

  • RDS Proxy — handles pooling outside your function. Worth its weight in tequila.

  • HTTP-only databases — DynamoDB, Aurora Data API. No connection state to manage.

2. Strip your bundle

Every megabyte adds download time. Common wins:

  • Don't ship the whole aws-sdk — use modular v3 imports or tree-shake.

  • Bundle with esbuild or webpack and minify.

  • Audit dev dependencies that snuck into production.

Going from 50MB to 5MB shaves ~200–400ms off cold start in most regions.

3. Defer heavy work past init

Lazily initialise anything you don't need on every request:

// Bad — runs on every cold start
const heavy = await loadConfigFromS3();

exports.handler = async (event) => {
  return process(event, heavy);
};

// Better — first request pays it, but only if needed
let configCache;
async function getConfig() {
  if (!configCache) configCache = await loadConfigFromS3();
  return configCache;
}

exports.handler = async (event) => {
  return process(event, await getConfig());
};

4. Provisioned Concurrency (when it makes sense)

This pre-warms N execution environments and keeps them hot. It costs money even when idle, so the question isn't really "how much traffic do I have" — it's "what does a cold start cost me?" If you're behind a synchronous user-facing API with a contractual p99, even a low-traffic function can justify PC. If you're processing SQS messages where 2s extra is invisible, even a high-traffic function probably can't. Work out the per-request cost of a cold start (lost conversions, SLA penalties, user rage) and compare it to PC's hourly bill. The maths usually answers itself.

5. SnapStart (Java & .NET)

If you're stuck on Java and can't drop to GraalVM native-image, AWS offers SnapStart — it snapshots the initialised runtime and resumes from disk. Cold starts drop from ~3s to ~200ms. Free if you're already on Java 11+. Catch: any randomness or open connections at init time need code changes.

6. ARM (Graviton)

Setting architectures: [arm64] in your function config gets you 20% cheaper invocations and, in our experience, ~10–15% faster cold starts due to better single-core perf on the init path. Confirm your Node native modules are built for arm64.

What probably won't help

A few cold-start "fixes" repeated as gospel that don't deliver:

  • CloudWatch ping schedules. Hitting your function every 5 minutes to "keep it warm" was a 2017 hack. Lambda concurrency means it warms one environment, not the N you'll need in a real traffic spike. Stop doing this.

  • Cranking memory to 10GB. More memory = more CPU = faster init, yes. But the cost curve gets ugly fast. Profile the actual init time and pick the smallest tier that meets your SLO.

  • Layers. Layers move code out of your zip, but they're layers don't meaningfully reduce cold-start cost — they're a deployment convenience.

  • Avoiding VPCs. This used to be real advice — VPC-attached Lambdas added 10+ seconds to cold starts because each environment had to attach its own ENI. AWS fixed this in 2019 with Hyperplane ENIs, which are shared and pre-attached. The penalty now is in the tens of milliseconds. If you're still architecting around "keep Lambdas out of the VPC," you're solving a 2018 problem.

How to actually measure

X-Ray gives you the breakdown via the Initialization subsegment. Lambda Insights gives you the percentile distribution. Don't fly blind:

aws logs filter-log-events \
  --log-group-name /aws/lambda/my-fn \
  --filter-pattern '"REPORT" "Init Duration"'

That tells you what your real cold-start cost is, in production, today. Anything else is folklore.

The honest take

Cold starts are mostly a non-problem for async backend processing where 1–2s extra latency is invisible. They become real problems for synchronous user-facing APIs where p99 latency is contractual. If that's you: profile first, fix init code, switch to ARM, reach for Provisioned Concurrency only if the maths works. If it's not you: stop worrying about it.

We use cookies to analyse site traffic and improve your experience. See our Privacy Policy for details.