Free — no signup required

The Lambda Lifecycle & Cold Starts

3 min read

The Two Phases of Every Invocation

Every Lambda invocation goes through one of two paths. Understanding which path you're on — and why — is the key to diagnosing latency problems.

Path 1 — Cold Start (Init Phase + Invoke Phase):

A cold start happens when no warm execution environment is available to handle the request. AWS must build one from scratch:

  1. Download your code from S3 (or ECR if you're using a container image).
  2. Start the MicroVM and boot the Linux kernel.
  3. Initialize the runtime (e.g., start the Python interpreter, load the JVM).
  4. Run your initialization code — everything outside your handler function: imports, global variable assignments, database connection setup, SDK client instantiation.
  5. Run your handler with the event payload.

Steps 1–4 are the Init Phase. Step 5 is the Invoke Phase. The user waits for all of it.

Cold start durations vary significantly by runtime. Python and Node.js cold starts typically add 100–500ms. Java with Spring Boot can add 6–15 seconds without optimization. Container image-based functions can add several seconds depending on image size.

Path 2 — Warm Start (Invoke Phase only):

If an execution environment from a previous invocation is still alive and idle, AWS reuses it. The Init Phase is skipped entirely. AWS passes the new event JSON directly to your handler. This is typically 1–10ms of overhead.

Optimizing for the Init Phase

The single most impactful Lambda optimization is structuring your code so expensive operations happen in the Init Phase, not the Invoke Phase. Since the Init Phase runs only once per execution environment (not once per request), any work done there is amortized across all subsequent warm invocations.

import boto3
import psycopg2

# ✅ GOOD: These run ONCE during Init Phase
# The execution environment reuses these across all warm invocations
s3_client = boto3.client('s3')
db_connection = psycopg2.connect(
    host="mydb.cluster.us-east-1.rds.amazonaws.com",
    database="orders",
    user="lambda_user",
    password="secret"
)

def handler(event, context):
    # ✅ GOOD: Reuses the connection initialized above
    cursor = db_connection.cursor()
    cursor.execute("SELECT * FROM orders WHERE id = %s", (event['order_id'],))
    return cursor.fetchone()
import boto3
import psycopg2

def handler(event, context):
    # ❌ BAD: Creates a new DB connection on EVERY invocation
    # This adds 50-200ms to every single request
    db_connection = psycopg2.connect(
        host="mydb.cluster.us-east-1.rds.amazonaws.com",
        database="orders",
        user="lambda_user",
        password="secret"
    )
    cursor = db_connection.cursor()
    cursor.execute("SELECT * FROM orders WHERE id = %s", (event['order_id'],))
    return cursor.fetchone()

Senior depth — the stale connection problem: Reusing database connections across warm invocations introduces a subtle failure mode. If the execution environment sits idle for several minutes, the database server may close the connection on its side (due to wait_timeout settings). Your next warm invocation will attempt to use a stale connection and fail. The fix is to implement connection validation before use — check if the connection is alive and reconnect if not. AWS RDS Proxy solves this at the infrastructure level by maintaining a connection pool that Lambda functions connect to, eliminating both the cold start connection penalty and the stale connection problem.

This is one of 18 chapters

Get every chapter — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

See pricing