Zum Inhalt springen

Cold Starts bei Serverless: Ursachen, Kosten und Gegenmaßnahmen

Cold Starts sind die versteckte Steuer des Serverless-Modells — Ursachen, Kosten in Latenz und praktische Strategien, um Funktionen warmzuhalten.

3 Min. Lesezeit
Latenzspitze durch einen Cold Start in einem Diagramm der Antwortzeiten einer Serverless-Funktion

Der erste Aufruf einer Serverless-Funktion nach einer Phase der Inaktivität dauert deutlich länger als die nachfolgenden Aufrufe. Dieser „Cold Start" umfasst das Bereitstellen eines Containers, das Laden der Runtime, das Initialisieren der Abhängigkeiten und das Ausführen des Initialisierungscodes. Bei AWS Lambda kann ein Cold Start je nach gewählter Runtime, Paketgröße und VPC-Konfiguration zwischen 100 ms und mehreren Sekunden hinzufügen.

Was bei einem Cold Start passiert

Ein Serverless-Aufruf folgt diesem Ablauf: Die Plattform empfängt die Anfrage, prüft, ob ein warmer Container verfügbar ist, und stellt andernfalls einen neuen bereit. Der kalte Pfad umfasst das Herunterladen des Deployment-Pakets, das Starten der Runtime und das Ausführen von Code auf Modulebene, bevor der Handler die Anfrage verarbeiten kann.

tstypescript
// Everything outside the handler runs during cold start
import { DynamoDB } from "@aws-sdk/client-dynamodb"; // ~200ms import
import { S3Client } from "@aws-sdk/client-s3";       // ~150ms import
 
const dynamodb = new DynamoDB({});  // Connection initialization
const s3 = new S3Client({});
 
// This handler runs on EVERY invocation (warm or cold)
export const handler = async (event: APIGatewayEvent) => {
  // Handler code here
  return { statusCode: 200, body: "OK" };
};

Cold-Start-Auswirkungen messen

Der Unterschied zwischen kalten und warmen Antworten lässt sich mit strukturiertem Logging messen.

tstypescript
// ❌ No visibility into cold starts
export const handler = async (event: unknown) => {
  const result = await processRequest(event);
  return result;
};
 
// ✅ Track cold starts explicitly
let isFirstInvocation = true;
 
export const handler = async (event: unknown) => {
  const start = Date.now();
  const isCold = isFirstInvocation;
  isFirstInvocation = false;
 
  const result = await processRequest(event);
 
  console.log(JSON.stringify({
    coldStart: isCold,
    duration: Date.now() - start,
    functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
    memoryAllocated: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
  }));
 
  return result;
};

Strategie 1 zur Minderung: Paketgröße reduzieren

Das Deployment-Paket muss heruntergeladen und entpackt werden, bevor die Runtime startet. Kleinere Pakete bedeuten schnellere Cold Starts.

tstypescript
// ❌ Importing the entire AWS SDK v2 (~70MB)
import AWS from "aws-sdk";
const dynamodb = new AWS.DynamoDB();
 
// ✅ Import only what you need (SDK v3 modular imports ~3MB)
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});

Tree-Shaking und das Bundling des Funktionscodes mit esbuild oder ähnlichen Tools können die Paketgröße drastisch reduzieren:

shbash
# Bundle with esbuild — output is a single file with only used code
npx esbuild src/handler.ts \
  --bundle \
  --platform=node \
  --target=node18 \
  --outfile=dist/handler.js \
  --minify \
  --external:@aws-sdk/*  # SDK v3 is included in Lambda runtime

Strategie 2 zur Minderung: Lazy Initialization (verzögerte Initialisierung)

Nicht jeder Aufruf benötigt jede Abhängigkeit. Aufwendige Ressourcen sollten nur dann initialisiert werden, wenn der jeweilige Codepfad sie tatsächlich benötigt.

tstypescript
// ❌ Always initializing S3 even if most invocations don't use it
const s3 = new S3Client({});
const dynamodb = new DynamoDBClient({});
 
export const handler = async (event: APIGatewayEvent) => {
  if (event.path === "/upload") {
    await s3.send(new PutObjectCommand(uploadParams));
  }
  // 90% of invocations only use DynamoDB
  return dynamodb.send(new GetItemCommand(params));
};
 
// ✅ Lazy initialization — S3 client created only when needed
const dynamodb = new DynamoDBClient({});
let _s3: S3Client | null = null;
 
function getS3Client(): S3Client {
  if (!_s3) _s3 = new S3Client({});
  return _s3;
}
 
export const handler = async (event: APIGatewayEvent) => {
  if (event.path === "/upload") {
    await getS3Client().send(new PutObjectCommand(uploadParams));
  }
  return dynamodb.send(new GetItemCommand(params));
};

Strategie 3 zur Minderung: Provisioned Concurrency

Für latenzkritische Pfade hält Provisioned Concurrency eine festgelegte Anzahl von Funktionsinstanzen dauerhaft warm — dadurch werden Cold Starts vollständig eliminiert, allerdings auf Kosten der Bezahlung für ungenutzte Rechenleistung.

ymlyaml
# serverless.yml — provisioned concurrency configuration
functions:
  api:
    handler: dist/handler.handler
    memorySize: 512
    provisionedConcurrency: 5  # 5 instances always warm
    events:
      - http:
          path: /api/{proxy+}
          method: ANY

Strategie 4 zur Minderung: Auswahl von Arbeitsspeicher und Runtime

Eine höhere Speicherzuweisung erhöht proportional die CPU-Leistung, wodurch sich die Initialisierung beschleunigt.

tstypescript
// CloudFormation / SAM template
// Doubling memory from 128MB to 256MB can cut cold start time in half
// The extra cost per invocation is often offset by faster execution
 
// Runtime comparison (approximate cold start overhead):
// Python 3.x:    ~200ms
// Node.js 18.x:  ~250ms
// Go 1.x:        ~100ms (compiled binary)
// Java 17:       ~800ms (JVM startup)
// .NET 6:        ~400ms
 
// Choose your runtime based on cold start tolerance

Cold Starts in einer VPC

Das Anbinden einer Lambda-Funktion an eine VPC verlängerte Cold Starts historisch um 5 bis 10 Sekunden, bedingt durch die Erstellung der ENI (Elastic Network Interface). AWS hat dies mit Hyperplane ENI verbessert, doch Funktionen innerhalb einer VPC weisen weiterhin eine höhere Cold-Start-Latenz auf. Eine Funktion sollte nur dann an eine VPC angebunden werden, wenn sie tatsächlich auf VPC-Ressourcen zugreifen muss.

Die wichtigsten Erkenntnisse

  1. Cold Starts sind Initialisierungs-Overhead – die Bereitstellung des Containers, der Start der Runtime und das Laden der Module tragen allesamt dazu bei
  2. Paketgröße reduzieren – Abhängigkeiten bündeln und per Tree-Shaking bereinigen, modulare SDK-Imports verwenden
  3. Ressourcenintensive Abhängigkeiten verzögert initialisieren – Clients nur für die Codepfade erstellen, die sie tatsächlich benötigen
  4. Provisioned Concurrency eliminiert Cold Starts – auf Kosten der Bezahlung für dauerhaft aktive Rechenleistung
  5. Mehr Speicher bedeutet schnellere Initialisierung – die CPU-Leistung skaliert linear mit dem zugewiesenen Speicher
  6. VPCs vermeiden, sofern nicht notwendig – die Anbindung an eine VPC verursacht weiterhin einen messbaren Cold-Start-Overhead
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX