Pay for provisioned concurrency when a synchronous, user-facing path has a latency budget that cold starts break, and only for the hours that path is genuinely busy. Everywhere else an idle pool is money spent on nothing. Provisioned concurrency keeps execution environments initialized before requests arrive; it does not make a warm invocation faster, and it does nothing for work that sits behind a queue.
What is AWS Lambda provisioned concurrency, exactly?
Provisioned concurrency tells Lambda to keep a fixed number of execution environments fully initialized and waiting. The runtime has booted, your top-level code has run, SDK clients are constructed and connection pools are open, all before a single request arrives. When a request lands, Lambda routes it into one of those environments and the handler starts immediately: the init phase never touches the request path.
Two configuration facts decide whether it works at all. It attaches to a published version or an alias, never to $LATEST, so a function with no versioning cannot use it and your deployments have to move the alias. And the environments are created when you configure the setting, not when traffic shows up, which is exactly why billing starts the moment they exist.
Provisioned concurrency vs reserved concurrency: what is the difference?
The names rhyme and the two settings do opposite things. Reserved concurrency is a ceiling: it carves a slice out of your account concurrency limit, guarantees that slice to one function, and throttles the function once the slice is full. It creates no warm environment whatsoever. It is a blast radius control, there so one runaway function cannot starve the rest of the account.
Provisioned concurrency is a pool of ready environments, which makes it a latency control. The two combine: reserved concurrency caps the function, provisioned concurrency pre-warms part of that cap. Setting reserved concurrency below the provisioned count is a configuration error, not a saving. One line to keep: reserved concurrency decides how much a function is allowed to run, provisioned concurrency decides how much of it is ready in advance.
What does provisioned concurrency fix, and what does it not?
It fixes one thing precisely: init landing on a user's request. On login, checkout or search-as-you-type, the p99 is usually a cold start paid for by a handful of unlucky users, and provisioned concurrency removes it up to the count you fund. That is the whole benefit.
What it does not do is speed up warm invocations. An environment that was already warm behaves identically either way: the handler runs the same code, queries the same database, waits on the same third party. If the p50 is slow, provisioned concurrency changes nothing and the problem lives in the handler. It does not shorten init duration either, it only moves init off the request path, so a heavy bundle stays heavy and every scale-out event pays for it again. We cover the measurement step in our article on Lambda warm starts, and the bundle work in our guide to reducing cold starts.
What happens when traffic exceeds the provisioned pool?
Nothing breaks: it spills over. Requests beyond the provisioned count are served by ordinary on-demand environments, which cold-start exactly as they would if the feature were switched off. No throttling, no error, just a slower request, which means an undersized pool degrades quietly instead of failing loudly.
That is why the count needs a metric rather than an opinion. Lambda publishes ProvisionedConcurrencyUtilization and ProvisionedConcurrencySpilloverInvocations, both described in the Lambda metrics documentation. Utilization pinned near its ceiling with steady spillover means the pool is too small. Utilization sitting low all day means you are paying for environments nobody uses. Alarm on both sides, because only one of them ever shows up in a latency dashboard.
Not sure whether provisioned concurrency would pay for itself on your workload? Describe your traffic shape: a one-page diagnosis within 48 hours.
Get my diagnosis →How do you stop paying for an idle pool?
Start by understanding what you are buying. Provisioned concurrency bills for every reserved environment for the entire time it is provisioned, whether it serves a request or not, plus a reduced duration rate on the requests it does serve. Current rates live on the Lambda pricing page. The consequence is blunt: a pool left at its daytime size overnight is pure waste, and a pool sized for peak season in the off season is the same waste with a better story.
Then hand the number to Application Auto Scaling. Two mechanisms exist and most production setups use both. Scheduled actions move the floor on a calendar: raise it before business hours, drop it at night and at the weekend, whenever the traffic curve is predictable. Target tracking follows utilization instead, adding environments as the pool fills and removing them as it empties, which covers the days the calendar gets wrong.
# Provisioned concurrency on an alias, sized by a schedule and a utilization target
Resources:
CheckoutFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs22.x
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 2
CheckoutTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
ServiceNamespace: lambda
ScalableDimension: lambda:function:ProvisionedConcurrency
ResourceId: !Sub "function:${CheckoutFunction}:live"
MinCapacity: 2
MaxCapacity: 40
ScheduledActions:
- ScheduledActionName: business-hours
Schedule: "cron(45 7 ? * MON-FRI *)"
Timezone: Europe/Paris
ScalableTargetAction:
MinCapacity: 10
CheckoutPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: pc-utilization
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref CheckoutTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 0.7
PredefinedMetricSpecification:
PredefinedMetricType: LambdaProvisionedConcurrencyUtilization
One caveat surprises people. Target tracking reacts to traffic that has already arrived, and freshly added environments still have to initialize before they can serve anything, so a vertical spike outruns the policy. When the spike is on the calendar, a sale, a payroll run, a broadcast slot, put it in a scheduled action instead of hoping the policy keeps up.
Provisioned concurrency, reserved concurrency, SnapStart or nothing?
Four options, one axis: what each one does to the init phase. Provisioned concurrency removes init from the request path and bills for the privilege. SnapStart replays a snapshot instead of running init, on the runtimes that support it. Reserved concurrency does nothing to init at all and is in this table only because it is constantly mistaken for a warm pool. Doing nothing is a legitimate answer for most functions.
| Criterion | Provisioned concurrency | Reserved concurrency | SnapStart | Nothing |
|---|---|---|---|---|
| What it does | Keeps N initialized environments ready | Caps the concurrency one function may use | Restores an initialized snapshot instead of running init | On-demand environments, init on first request |
| Effect on cold starts | Removed up to the provisioned count | None, it is not a warm pool | Init time cut sharply on supported runtimes | Full init on every scale-out |
| Runtimes | Any | Any | Java, Python and .NET; check the docs for others | Any |
| Cost shape | Hourly per reserved environment, invoked or not, plus a reduced duration rate | Free, it only redistributes your account limit | No extra charge on Java; caching and restore fees on Python and .NET | Standard invocation and duration billing |
| Main constraint | Versions and aliases only; an idle pool is pure waste | Throttles the function once the slice is full | Published versions only; exclusive with provisioned concurrency | The p99 carries the init |
| When it wins | Synchronous user-facing paths with a real latency budget | Protecting the account from one noisy function | Slow-initializing runtimes where an hourly pool is not justified | Async work, batch, cron, anything behind a queue |
On Java, Python and .NET, price SnapStart before committing to an hourly pool: the two are mutually exclusive on the same function, so this is a decision, not a stack.
When should you not pay for provisioned concurrency?
Whenever nobody is waiting. Queue consumers, S3 and DynamoDB stream handlers, EventBridge cron jobs, nightly exports, webhook handlers that answer 202 and finish later: every one of them absorbs a cold start without a single user noticing. Paying an hourly rate to shave init off a batch job is the most common way we see a Lambda bill grow while nothing gets faster.
Three more cases where it is the wrong tool. Traffic that is steady and dense enough already keeps environments warm on its own, so there are few cold starts left to buy back. Traffic that is genuinely unpredictable, with long quiet stretches and rare spikes, forces a losing choice: a pool sized for the spike idles most of the time, a pool sized for the average spills over anyway, and a scheduled floor cannot help when the calendar means nothing. And a function whose init drags because of a bloated bundle or a heavy framework should be fixed, not subsidised. If the real question is Lambda against a container that runs continuously, that is a different trade-off, and we take it from the top in our Fargate versus Lambda comparison.
Decision checklist
- Synchronous, user-facing, with a latency budget the p99 misses: provisioned concurrency earns its keep.
- Async, batch, cron or anything behind a queue: do not buy it, at any price.
- Measure init duration and cold-start frequency first, and shrink the bundle before paying anything.
- Configure it on an alias, never on
$LATEST, and deploy through that alias. - Hand the count to Application Auto Scaling: a scheduled floor for the known curve, target tracking for the rest.
- Alarm on utilization and on spillover, so an oversized pool is as visible as an undersized one.
- On Java, Python and .NET, evaluate SnapStart before committing to an hourly pool.
- Re-check the official pricing page before modelling anything: rates and free allowances move.