Marcelo Pastorino, Software Developer

Serverless Cold Starts

Latency spikes when a function instance boots before handling its first request.

A serverless cold start happens when a platform must create or initialize a new function instance before it can run your handler. The first request after idle time pays the cost of loading the runtime, dependencies, and application code.

Why it matters

Cold starts add unpredictable latency on top of your handler logic. User-facing APIs with tight SLAs feel the spike even when average latency looks fine. Measure P99, not the mean, because one slow request is what users remember.

What drives cold-start time

  • Runtime choice, heavier runtimes (for example JVM or .NET) typically boot slower than lightweight ones.
  • Bundle size, large dependency trees increase download and initialization time.
  • Module-scope work, imports and setup that run at load time run on every cold start.
  • VPC configuration, attaching functions to a VPC can add network setup latency.
  • Concurrency, when all warm instances are busy, new requests trigger fresh instances.

Mitigations

  • Keep deployment packages small; tree-shake and split optional code paths.
  • Move heavy initialization behind lazy loaders or warm-up paths only when needed.
  • Use provisioned concurrency (or platform equivalents) for predictable latency on critical endpoints at a direct cost.
  • Prefer asynchronous or queued work for non-latency-sensitive tasks.
  • Right-size memory; some platforms scale CPU with memory, which can shorten init time.
// Avoid heavy work at module scope
let db;

async function getDb() {
  if (!db) db = await connectDatabase();
  return db;
}

export async function handler(event) {
  const database = await getDb();
  return database.query(event.path);
}
// Avoid heavy work at module scope
let db: Database | undefined;

async function getDb(): Promise<Database> {
  if (!db) db = await connectDatabase();
  return db;
}

export async function handler(event: ApiEvent) {
  const database = await getDb();
  return database.query(event.path);
}
// Lazy-init expensive clients inside the handler scope
private static Database? _db;

public async Task<ApiResponse> HandlerAsync(ApiEvent evt)
{
    _db ??= await ConnectDatabaseAsync();
    return await _db.QueryAsync(evt.Path);
}
# Lazy-init expensive clients instead of at import time
_db = None

async def get_db():
    global _db
    if _db is None:
        _db = await connect_database()
    return _db

async def handler(event):
    db = await get_db()
    return await db.query(event["path"])

Trade-offs

  • Provisioned concurrency removes variance but bills for idle capacity.
  • Smaller bundles help cold starts but can push complexity into custom packaging.
  • Over-warming every function wastes money; target paths where latency SLAs justify it.

See also

On Wikipedia