AWS Lambda Provisioned Concurrency: Eliminating Cold Starts
Lambda cold starts — that 500ms to 10-second delay when a new execution environment initializes — are the biggest complaint about serverless. For most workloads, cold starts are a rounding error. But for API endpoints with strict latency SLAs, payment processing functions, or interactive applications, a multi-second delay on the first request is unacceptable. Provisioned concurrency keeps a pool of pre-initialized execution environments warm and ready, eliminating cold starts entirely.
What Causes Cold Starts?
A cold start happens when Lambda creates a new execution environment. The full cycle:
- Download code — Lambda fetches your deployment package from S3
- Start runtime — Initialize the language runtime (JVM, .NET CLR, Node.js V8)
- Run init code — Execute code outside the handler (imports, DB connections, SDK clients)
- Execute handler — Process the actual request
| Runtime | Typical Cold Start | With VPC | With Provisioned Concurrency |
|---|---|---|---|
| Python 3.9 | 200-500ms | +1-2s | ~0ms |
| Node.js 18 | 150-400ms | +1-2s | ~0ms |
| Java 17 | 2-8s | +2-4s | ~0ms |
| .NET 6 | 400-800ms | +1-2s | ~0ms |
Java and .NET suffer the worst because their runtimes require JIT compilation. Provisioned concurrency pre-runs all initialization steps, so when a request arrives, the handler executes immediately on an already-warm environment.
Configuring Provisioned Concurrency
Enable provisioned concurrency on a published version or alias (not $LATEST):
# Publish a new version
aws lambda publish-version \
--function-name payment-processor \
--description "v2.1.0 - optimized checkout"
# Set provisioned concurrency on that version
aws lambda put-provisioned-concurrency-config \
--function-name payment-processor \
--qualifier 3 \
--provisioned-concurrent-executions 50
# Check provisioned concurrency status
aws lambda get-provisioned-concurrency-config \
--function-name payment-processor \
--qualifier 3
Or configure via an alias (recommended for deployments):
# Create/update alias pointing to the version
aws lambda update-alias \
--function-name payment-processor \
--name production \
--function-version 3
# Set provisioned concurrency on the alias
aws lambda put-provisioned-concurrency-config \
--function-name payment-processor \
--qualifier production \
--provisioned-concurrent-executions 50
Terraform configuration:
resource "aws_lambda_function" "payment_processor" {
function_name = "payment-processor"
runtime = "java17"
handler = "com.example.PaymentHandler::handleRequest"
filename = "payment-processor.jar"
memory_size = 1024
timeout = 30
environment {
variables = {
DB_ENDPOINT = var.db_endpoint
STAGE = "production"
}
}
}
resource "aws_lambda_alias" "production" {
name = "production"
function_name = aws_lambda_function.payment_processor.function_name
function_version = aws_lambda_function.payment_processor.version
}
resource "aws_lambda_provisioned_concurrency_config" "production" {
function_name = aws_lambda_function.payment_processor.function_name
provisioned_concurrent_executions = 50
qualifier = aws_lambda_alias.production.name
}
Auto-Scaling Provisioned Concurrency
Static provisioned concurrency wastes money during low-traffic periods. Use Application Auto Scaling to adjust based on utilization:
resource "aws_appautoscaling_target" "lambda_target" {
max_capacity = 200
min_capacity = 10
resource_id = "function:${aws_lambda_function.payment_processor.function_name}:${aws_lambda_alias.production.name}"
scalable_dimension = "lambda:function:ProvisionedConcurrency"
service_namespace = "lambda"
}
# Scale based on utilization — target 70%
resource "aws_appautoscaling_policy" "lambda_policy" {
name = "lambda-provisioned-concurrency"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.lambda_target.resource_id
scalable_dimension = aws_appautoscaling_target.lambda_target.scalable_dimension
service_namespace = aws_appautoscaling_target.lambda_target.service_namespace
target_tracking_scaling_policy_configuration {
target_value = 0.7 # Scale up when 70% of provisioned capacity is used
predefined_metric_specification {
predefined_metric_type = "LambdaProvisionedConcurrencyUtilization"
}
scale_in_cooldown = 60
scale_out_cooldown = 0
}
}
# Scheduled scaling for known traffic patterns
resource "aws_appautoscaling_scheduled_action" "morning_rush" {
name = "morning-scale-up"
service_namespace = aws_appautoscaling_target.lambda_target.service_namespace
resource_id = aws_appautoscaling_target.lambda_target.resource_id
scalable_dimension = aws_appautoscaling_target.lambda_target.scalable_dimension
schedule = "cron(0 8 ? * MON-FRI *)"
scalable_target_action {
min_capacity = 100
max_capacity = 200
}
}
resource "aws_appautoscaling_scheduled_action" "evening_scale_down" {
name = "evening-scale-down"
service_namespace = aws_appautoscaling_target.lambda_target.service_namespace
resource_id = aws_appautoscaling_target.lambda_target.resource_id
scalable_dimension = aws_appautoscaling_target.lambda_target.scalable_dimension
schedule = "cron(0 20 ? * MON-FRI *)"
scalable_target_action {
min_capacity = 10
max_capacity = 50
}
}
This configuration keeps 10 environments warm during off-hours and scales to 100-200 during business hours. Target tracking at 70% means auto-scaling adds capacity before you run out of warm environments.
Cost Analysis: When It Makes Sense
Provisioned concurrency has a separate pricing model:
Provisioned: $0.0000041667 per GB-second (~$0.015/GB-hour)
On-demand: $0.0000166667 per GB-second (~$0.06/GB-hour)
Provisioned is ~4x cheaper per execution hour BUT you pay 24/7
Break-even calculation:
# 1024MB function, provisioned concurrency of 50
provisioned_cost_per_hour = 50 * 1.0 * 0.0000041667 * 3600 # $0.75/hour
provisioned_cost_per_month = provisioned_cost_per_hour * 24 * 30 # $540/month
# On-demand equivalent — how many invocations to break even?
# If average duration is 200ms:
on_demand_per_invocation = 1.0 * 0.2 * 0.0000166667 # $0.0000033
break_even_invocations = provisioned_cost_per_month / on_demand_per_invocation
# ~163 million invocations/month to break even on cost alone
Provisioned concurrency is not a cost optimization — it’s a latency optimization. Use it when:
- P99 latency SLAs prohibit cold starts
- Java/.NET functions with 3-10 second cold starts power interactive APIs
- VPC-attached functions need sub-second response times
- Downstream services (databases, APIs) can’t handle burst connections from simultaneous cold starts
Best Practices
- Use aliases, not version numbers — point provisioned concurrency at an alias like
production. When you deploy a new version, update the alias. Provisioned concurrency transfers to the new version automatically during the alias update. - Optimize your init code first — before paying for provisioned concurrency, reduce cold start times. Lazy-load SDK clients, minimize dependencies, use lighter frameworks. A 200ms cold start might not need provisioned concurrency at all.
- Monitor ProvisionedConcurrencySpilloverInvocations — this CloudWatch metric shows requests that exceeded your provisioned capacity and fell back to on-demand (with cold starts). If spillover is high, increase your provisioned concurrency or auto-scaling limits.
- Combine with SnapStart for Java — Lambda SnapStart (Java 11+) snapshots the initialized JVM state. Combined with provisioned concurrency, Java cold starts drop from seconds to sub-100ms even for on-demand overflow invocations.
- Don’t provision $LATEST — provisioned concurrency requires a published version or alias. Using aliases with weighted routing lets you canary-deploy new versions while maintaining warm environments for the stable version.
Conclusion
Provisioned concurrency eliminates Lambda cold starts by maintaining pre-initialized execution environments. Auto-scaling adjusts capacity based on utilization or schedules, preventing waste during low-traffic periods. The cost is significant — you pay for provisioned GB-seconds 24/7 — so reserve it for latency-sensitive workloads where cold starts violate SLAs. For Java and .NET functions behind synchronous APIs, provisioned concurrency turns Lambda into a warm, responsive compute platform.
Key Takeaways:
- Provisioned concurrency pre-initializes execution environments, eliminating cold starts
- Configure on aliases (not
$LATEST) for seamless deployments - Auto-scaling with target tracking and scheduled actions optimizes cost
- Monitor
ProvisionedConcurrencySpilloverInvocationsto detect capacity shortfalls
Frequently Asked Questions
Q: Does provisioned concurrency work with Lambda@Edge? No. Lambda@Edge doesn’t support provisioned concurrency. CloudFront functions and Lambda@Edge rely on AWS’s global replication for low-latency execution. For latency-sensitive edge processing, use CloudFront Functions (JavaScript only, sub-millisecond starts) or keep Lambda@Edge functions lightweight with minimal dependencies.
Q: How long does it take to provision environments?
Typically 1-5 minutes depending on function size, memory, and runtime. Java functions with large classpaths take longer. The get-provisioned-concurrency-config API shows the status — it transitions from IN_PROGRESS to READY. Plan deployments accordingly and don’t delete the old version until the new one is ready.
Q: Can I use provisioned concurrency with Lambda container images? Yes. Container image-based Lambda functions support provisioned concurrency the same way as ZIP-based functions. Container images can have higher cold start times (due to larger image sizes), making provisioned concurrency especially valuable for containerized Lambda functions.