Lambda cold starts: when the delay is real and when it isn't
A client's mobile app loads a product listing in 180ms on the first tap and 40ms on every tap after. The 140ms gap is a Lambda cold start. Their product lead flagged it as a performance problem. It wasn't.
Cold starts happen when AWS assigns a fresh container to handle a request after the previous one was reclaimed. The initialization cost depends on the runtime, the memory allocation, and what your handler does at module scope. A Python Lambda with 128MB of memory importing boto3 and a database driver can take 800ms to warm up. The same function at 512MB might drop to 300ms. A Node function importing only the AWS SDK v3 modular packages might cold-start in 200ms.
Whether that cost matters depends on who is waiting and for what.
The case where it genuinely hurts: a synchronous user action with no acceptable fallback. A customer taps "Generate Quote" and waits for a response before they can proceed. The API Gateway request hits a Lambda that was idle for ten minutes, and the user sees a spinner for 900ms instead of 200ms. If that path is the primary conversion step and it runs cold on a meaningful percentage of requests, the delay is real. Users on mid-range Android devices over flaky 4G will feel a sub-second delay more acutely than your dev machine suggests.
The case where it usually doesn't matter: background and asynchronous paths. Webhook receivers, SQS consumers, scheduled cleanup jobs, SNS-triggered image processing. Nobody is staring at a spinner. An SQS-driven Lambda that takes 1.2 seconds to cold-start before processing a batch of ten messages is fine if your queue depth is healthy and your consumers can keep up. The cold start adds latency to the first batch, not to the user.
The case that gets over-flagged: read-only API endpoints behind a cache. A product listing endpoint that cold-starts in 300ms but serves 95% of its traffic from ElastiCache at 8ms is not a cold-start problem. The five percent of cache-miss requests that hit a cold Lambda are spread across users and time. Optimizing the cold start here yields a 0.15% improvement in p50 latency and zero improvement in p99 for cache-hit traffic.
When the delay is real, the fixes are straightforward and we usually apply them in this order. Provisioned concurrency eliminates cold starts on a fixed number of instances but adds cost proportional to how many you keep warm and for how long. For a quote-generation endpoint that gets traffic during business hours, provisioning two concurrent executions from 9am to 7pm costs roughly $15-25 per month depending on memory size. That is cheaper than rewriting the function.
Increasing memory allocation is the second lever. Lambda CPU scales with memory, so bumping from 256MB to 1GB often cuts cold-start time in half while doubling per-invocation cost. For a function that runs 50,000 times a day and cold-starts maybe 200 of those, the cost increase is negligible.
The third lever is reducing module-scope work. Moving database connections and heavy imports out of the handler and into the init phase is already standard practice, but we still see code that initializes a Sequelize models object on every invocation. That adds 150ms to every cold start for no reason.
What we do not recommend is the DIY keep-warm pattern of scheduling a CloudWatch event to ping the Lambda every five minutes. It works, but provisioned concurrency does the same thing with a billing model you can predict, and AWS has improved its pricing so you pay a lower rate for provisioned concurrency duration than for on-demand. The keep-warm cron approach also doesn't handle scale-out: warming one instance doesn't help when traffic spikes and Lambda spins up five new ones.
Measure before optimizing. Enable X-Ray tracing on the function, send enough traffic to generate cold starts, and look at the actual init segment in the trace. If init time is 80ms and your total response time is 400ms, the cold start is not your problem. The database query is.