Fixing Lambda Cold Starts: The Cost Math of Provisioned Concurrency

5 min read

Cold starts are the most common worry raised against Lambda. The conclusion up front: most workloads are fine with the free fixes (smaller packages, lighter runtimes, SnapStart), and Provisioned Concurrency is something you price out only for user-facing paths with an explicit p99 requirement. It has a reputation as an expensive insurance policy, but its execution rate is deliberately set below on-demand, so there is a crossover where high utilization makes the total cheaper, not more expensive. Whether Lambda is the right compute at all is covered in Lambda vs Fargate. Prices are for us-east-1.

Remedies side by side #

RemedyAdded costEffectApplies to
Trim package and dependenciesNoneShorter initAll runtimes
More memoryHigher execution rateCPU scales with it, faster initCPU-bound init
SnapStartFree for Java; cache/restore fees for Python and .NETSeconds → low hundreds of msJava, Python, .NET
Provisioned Concurrency$0.0000041667 per GB-second, always onEffectively removes cold startsPaths with p99 requirements
Scheduled warm-up pingsSmall invocation feesIncomplete (fails under concurrency)Not recommended

Anatomy of a cold start #

When a request arrives and no idle execution environment exists, Lambda builds one: code download, runtime boot, then the initialization code outside the handler. That init phase is the bulk of cold-start latency — under 100ms at best, several seconds for Java or .NET with heavy frameworks.

What matters as much is frequency. Cold starts happen on the first request, when concurrency scales up, and right after deploys. For a function with steady traffic, they are often under 1% of invocations, so the first question is whether the problem is p99, not the average. For internal batch jobs and async event consumers, a cold start of a few seconds is not even a problem.

Before spending money, the basics: shrink the deployment package and dependencies so download and init are short, keep the code outside the handler (SDK clients, config loading) to the minimum, and if init is CPU-bound, raise memory to get proportionally more CPU. SnapStart restores a pre-initialized snapshot instead of initializing from scratch; it is free for Java, which makes it the first thing to try on Java functions. Python and .NET are supported too, but snapshot caching and restores are billed, so check the fees on high-volume functions before enabling it.

Provisioned Concurrency: two-part pricing #

Provisioned Concurrency keeps the number of execution environments you specify initialized and waiting. The pricing has two parts.

  • Keep-warm fee: $0.0000041667 per GB-second on the provisioned capacity, billed whether or not it runs. Keeping one 1GB environment warm for a month is about $11.
  • Execution fee: time executed on provisioned environments bills at $0.0000097222 per GB-second, about 58% of the on-demand rate ($0.0000166667).

The discounted execution rate is the key. If provisioned capacity sits idle, the keep-warm fee is pure loss; as utilization rises, the execution discount claws it back. The total matches on-demand at roughly 60% utilization. If steady traffic keeps your provisioned capacity more than 60% busy, removing cold starts comes with savings attached. At 10% utilization, you are simply paying most of the keep-warm fee for the latency guarantee.

A worked example: provisioning concurrency of 10 on a 1GB function costs about $110 a month to keep warm. Run it at an average 70% and the execution discount recovers about $89 versus on-demand, for a net cost of about $21. Run it at 20% and the discount is only about $25, leaving about $85 as the price of cold-start removal.

Two operational notes. Traffic beyond the provisioned capacity spills over to on-demand, where cold starts return. And Application Auto Scaling can schedule the capacity, so keeping it warm only during business hours cuts the keep-warm fee to a third — the default move for internal APIs with no night traffic.

As for the old trick of pinging the function every few minutes with EventBridge: it warms exactly one environment, and any concurrent burst goes straight to cold starts. It is not a substitute for Provisioned Concurrency.

Decision order #

  1. Confirm p99 is the problem: check init duration and cold-start ratio in CloudWatch. For async and batch paths, accepting cold starts is the correct fix.
  2. Exhaust the free fixes first: package trim, init cleanup, SnapStart on Java. If p99 lands inside the requirement here, you are done.
  3. Price Provisioned Concurrency from capacity and utilization: size it from peak concurrent executions, and compare totals against on-demand using 60% utilization as the reference line.
  4. Schedule the capacity: with a clear traffic pattern, Auto Scaling schedules erase the nights-and-weekends keep-warm fee.
  5. At sustained high traffic, revisit the architecture too: high utilization makes Provisioned Concurrency a saving, but at that scale the comparison against an always-on Fargate container opens up again as well.

Summary #

  • The init phase is the substance of a cold start, and it usually affects under 1% of invocations. Establish a p99 requirement before spending anything.
  • Free fixes come first: smaller packages, leaner init, SnapStart on Java. For Python and .NET, check SnapStart’s cache and restore fees before enabling.
  • Provisioned Concurrency is two-part: $0.0000041667/GB-s to keep warm, with execution at about 58% of the on-demand rate. Break-even sits near 60% utilization.
  • At low utilization the keep-warm fee is the price of latency, and scheduling shrinks it. Warm-up pings fail under concurrency and are not a substitute.
  • What sits in front of the API — API Gateway or an ALB, with their different pricing models — is the next post.
X