Provisioned concurrency keeps a chosen number of Lambda execution environments initialized and waiting, so requests landing on them never see a cold start. The catch is the billing model: you pay for the provisioned capacity by the GB-hour for every hour it is configured, whether one request arrives or a million. That makes it a straightforward economic decision — worth it for latency-sensitive synchronous traffic with a predictable floor, waste for spiky low-volume functions, and irrelevant for async workloads where nobody feels the cold start.
What you are deciding
On-demand Lambda initializes environments reactively: traffic arrives, environments spin up, the first request on each one eats the init time. Provisioned concurrency flips this to proactive: you declare “keep N environments warm,” Lambda runs the init phase ahead of time, and requests routed to provisioned environments start executing immediately. Requests beyond N spill over to regular on-demand environments — they still work, they just cold-start, and they show up in the ProvisionedConcurrencySpilloverInvocations metric.
The cost structure has two parts. You pay a per GB-hour rate for the configured capacity, continuously. Invocations that run on provisioned environments are then billed for duration at a lower per-GB-second rate than on-demand. The consequence: at high, steady utilization, provisioned capacity can cost about the same as — or less than — the equivalent on-demand usage, so the latency win comes nearly free. At low utilization you are renting empty warm rooms. Model your own numbers in https://calculator.aws/ before committing; the crossover depends on utilization, memory size, and region.
Two operational details shape the decision. Provisioned concurrency attaches to a published version or an alias — never to $LATEST — so it forces a versioned deployment discipline (which you should have anyway). And it can be scheduled or auto-scaled through Application Auto Scaling, which is how you avoid paying for the 3 a.m. trough.
Also weigh the alternative that did not exist when this feature launched: SnapStart now covers Java, Python, and .NET, resuming from a snapshot instead of keeping environments warm. It does not guarantee zero cold start, but it cuts init to a few hundred milliseconds without per-hour capacity charges — for many workloads that is good enough and far cheaper.
What to check first
- Whether anyone actually feels the cold starts. Synchronous, user-facing, latency-sensitive: candidates. Queue consumers and event pipelines: not candidates, at any price.
- The real cold start rate and init duration. A 300 ms init hitting 0.4% of requests rarely justifies fixed spend; a 4 s init hitting 5% of requests on a checkout API does.
- The traffic floor. Provisioned concurrency pays for itself against the steady portion of traffic. Read two weeks of
ConcurrentExecutionsfor the function and find the trough and the plateau. - Whether you deploy through aliases. If everything runs on
$LATEST, that changes first.
How to verify
Establish the baseline before spending. Cold start frequency and init cost, from Logs Insights:
filter @type = "REPORT"
| stats count() as invocations,
sum(strcontains(@message, "Init Duration")) as coldStarts,
pct(@initDuration, 95) as p95InitMs
by bin(1h)
Concurrency shape, from CloudWatch:
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda --metric-name ConcurrentExecutions \
--dimensions Name=FunctionName,Value=my-function \
--start-time 2026-06-20T00:00:00Z --end-time 2026-07-04T00:00:00Z \
--period 3600 --statistics Maximum Average
After enabling, watch the two metrics that grade the sizing: ProvisionedConcurrencyUtilization (how much of what you pay for is used) and ProvisionedConcurrencySpilloverInvocations (how often traffic exceeds it). Both are visible under Lambda > Functions > (function) > Monitor once configured; the feature itself is documented at https://docs.aws.amazon.com/lambda/.
The approach
- Qualify the function honestly. It must be synchronous, latency-sensitive, and measured — you know its cold start rate, init duration, and concurrency floor. If you skipped the measurement, do that first; half the time the data kills the request.
- Publish a version and route through an alias.
aws lambda publish-version --function-name my-functionthen point an alias likeliveat it. Callers invoke the alias. - Size to the plateau, not the peak. Set provisioned concurrency at roughly the steady-state concurrency; let spillover absorb the spikes.
aws lambda put-provisioned-concurrency-config --function-name my-function --qualifier live --provisioned-concurrent-executions 25. Sizing to the peak is how the bill doubles for latency nobody measured. - Schedule the capacity to follow traffic. Register the alias with Application Auto Scaling and add scheduled actions (business hours up, nights and weekends down) or target tracking at ~70%
ProvisionedConcurrencyUtilization. A flat 24/7 setting is almost never the right shape. - Wire deployments to shift capacity. Each release publishes a new version; the alias moves, and provisioned concurrency must follow it — SAM, CDK, and Terraform all support this, and gradual alias shifting pairs naturally with canary deployments via CodeDeploy hooks.
- Review utilization after two weeks. Below ~40% average utilization, shrink it or question the whole arrangement; sustained spillover during the window you paid to protect means the floor was set too low.
When this is the wrong move
Async and queue-driven workloads: the cold start is invisible, so the spend buys nothing. Dev and staging environments: warm capacity for zero users is a classic bill-audit finding. Spiky-and-rare traffic (a function that bursts to 200 for five minutes a day): scheduled provisioned concurrency for the known window can work, but if the spikes are unpredictable, spillover cold starts will dominate anyway. And if the underlying complaint is a 4-second init, cutting the init — smaller package, SnapStart, lazy imports — is cheaper than renting your way around it.
Common mistakes
- Attaching provisioned concurrency to a version, deploying a new version, and paying to keep the old code warm while all traffic cold-starts on the new one. Use aliases and move capacity with the deploy.
- Sizing to peak traffic and running single-digit utilization percentages forever.
- Forgetting reserved and provisioned concurrency interact with the account pool — provisioned capacity counts against the regional concurrency limit.
- Enabling it to fix p99 latency that turns out to be a slow dependency, not cold starts. The Logs Insights breakdown settles this in ten minutes.
Keeping it under control
- Alarm on
ProvisionedConcurrencyUtilizationbelow ~30% for a week (paying for air) and on sustained spillover (under-provisioned). - Tag provisioned-concurrency spend and review it in the monthly cost pass alongside the other fixed commitments.
- Re-run the cold start measurement quarterly; a package diet or SnapStart adoption can quietly remove the original justification.
- Keep dev/staging at zero provisioned concurrency by policy.
Provisioned concurrency is the honest version of every warming hack: real reserved capacity, at a real price, for traffic where latency is worth real money — buy it where that sentence is true and nowhere else. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.