AWS Lambda - zimny start powoduje 504 Gateway Timeout na API Gateway
Naprawa 504 Timeout na API Gateway spowodowanego przez zimny start Lambda: diagnoza VPC ENI, optymalizacja pakietu, provisioned concurrency, SnapStart dla Java.
Ten runbook opisuje diagnozę i eliminację problemu zimnego startu Lambda powodującego timeouty na API Gateway. Koszty serverless i optymalizację Lambda omawiamy w artykule Serverless bez finansowych niespodzianek - Lambda pricing. Jeśli potrzebujesz pomocy z architekturą serverless - umów konsultacje.
Objaw
API Gateway zwraca HTTP 504 sporadycznie, korelując z pierwszymi żądaniami po okresie bezczynności:
# Sprawdź metryki 5xx na API Gateway
aws cloudwatch get-metric-statistics \
--namespace AWS/ApiGateway \
--metric-name 5XXError \
--dimensions Name=ApiName,Value=my-api \
--start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics Sum
# Sprawdź IntegrationLatency - kluczowa metryka (ile czeka na Lambda)
aws cloudwatch get-metric-statistics \
--namespace AWS/ApiGateway \
--metric-name IntegrationLatency \
--dimensions Name=ApiName,Value=my-api \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Maximum
# Cold start: Maximum > 10000ms (10s+)
# Warm: Maximum < 500ms
# Lambda Duration vs Init Duration
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value=my-api-function \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Maximum
# Max > 29000 = przekracza timeout API Gateway
# Szukaj Init Duration w CloudWatch Logs (cold start marker)
aws logs filter-log-events \
--log-group-name /aws/lambda/my-api-function \
--start-time $(date -u -d '24 hours ago' +%s)000 \
--filter-pattern "Init Duration" \
--max-items 20 \
--query 'events[].message'
# Format: "REPORT ... Init Duration: 8523.45 ms ..."
# Init Duration > 10000ms = VPC cold start lub duży package
Charakterystyka zimnego startu vs rozgrzana instancja:
| Metryka | Cold start | Warm invocation |
|---|---|---|
| Init Duration | 3-30s (zależnie od runtime + VPC) | 0 (brak) |
| Duration (total) | Init + execution time | Tylko execution time |
| Billed Duration | Init + execution | Execution |
| API Gateway IntegrationLatency | Init + execution > 29s = 504 | < 500ms (typowo) |
Przyczyna
Zimny start Lambda to czas inicjalizacji nowej instancji środowiska wykonawczego. Składa się z:
1. Pobranie i dekompresja pakietu wdrożeniowego:
- Mały package (5 MB): ~200ms
- Duży package (50 MB, np. Python z numpy/pandas): ~2-5s
- Package z Lambda Layer: dodatkowy czas per layer
2. Runtime initialization (interpreter/JVM start):
- Python: ~200-500ms
- Node.js: ~100-300ms
- Java (zimny start JVM): ~3-8s
- .NET: ~1-3s
3. VPC ENI attachment (jeśli Lambda jest w VPC):
- Hyperplane ENI: ~1-2s (po 2019 improvement)
- Stare konta/konfiguracje: do 10-15s
- Cross-AZ: dodatkowe ~500ms
4. Handler initialization code (import modules, DB connections):
- Import heavy libraries (PyTorch, TensorFlow): 5-15s
- Nawiązanie połączenia DB/Redis: 1-3s
- Download secrets z Secrets Manager: 0.5-1s
Suma: VPC Lambda z dużym pakietem w Java może mieć zimny start 15-30s → przekroczenie 29s timeout API Gateway.
5. Kiedy zimny start się pojawia:
- Pierwszy request po deploy (wszystkie instancje zimne)
- Po ~5-15 min bezczynności (AWS recykluje instancję)
- Przy nagłym spike’u ruchu (nowe instancje startują równolegle)
- Po skalowaniu w dół do 0
Rozwiązanie
A) Provisioned Concurrency - eliminacja zimnego startu:
# Włącz provisioned concurrency na opublikowanej wersji/alias
# 1. Opublikuj wersję
VERSION=$(aws lambda publish-version \
--function-name my-api-function \
--query 'Version' --output text)
# 2. Utwórz/aktualizuj alias 'live' wskazujący na wersję
aws lambda update-alias \
--function-name my-api-function \
--name live \
--function-version $VERSION
# 3. Ustaw provisioned concurrency na alias
aws lambda put-provisioned-concurrency-config \
--function-name my-api-function \
--qualifier live \
--provisioned-concurrent-executions 5
# 4. Sprawdź status (musi być "Ready")
aws lambda get-provisioned-concurrency-config \
--function-name my-api-function \
--qualifier live \
--query '{status:Status, requested:RequestedProvisionedConcurrentExecutions, available:AvailableProvisionedConcurrentExecutions}'
# 5. Skieruj API Gateway na alias (nie $LATEST!)
# W API Gateway integration URI zmień na:
# arn:aws:lambda:eu-west-1:123456789012:function:my-api-function:live
Autoscaling provisioned concurrency (dla zmiennego ruchu):
# Zarejestruj target do Application Auto Scaling
aws application-autoscaling register-scalable-target \
--service-namespace lambda \
--resource-id function:my-api-function:live \
--scalable-dimension lambda:function:ProvisionedConcurrency \
--min-capacity 2 \
--max-capacity 50
# Target tracking - utrzymuj utilization ~70%
aws application-autoscaling put-scaling-policy \
--service-namespace lambda \
--resource-id function:my-api-function:live \
--scalable-dimension lambda:function:ProvisionedConcurrency \
--policy-name target-tracking-70pct \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 0.7,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "LambdaProvisionedConcurrencyUtilization"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
B) Optymalizacja pakietu wdrożeniowego (redukuj czas inicjalizacji):
# Sprawdź aktualny rozmiar package
aws lambda get-function \
--function-name my-api-function \
--query 'Configuration.{CodeSize:CodeSize, Layers:Layers[].LayerArn, MemorySize:MemorySize, Runtime:Runtime}'
# Python - użyj Lambda Powertools Layer zamiast bundlowania
# Node.js - tree-shaking z esbuild
# Java - użyj GraalVM native-image lub SnapStart
# Dockerfile - multi-stage build dla minimalnego image (container Lambda)
FROM public.ecr.aws/lambda/python:3.12 as builder
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -t /opt/python/
# Usuń niepotrzebne pliki (testy, docs, cache)
RUN find /opt/python -name "*.pyc" -delete && \
find /opt/python -name "__pycache__" -type d -exec rm -rf {} + && \
find /opt/python -name "tests" -type d -exec rm -rf {} + && \
find /opt/python -name "*.dist-info" -type d -exec rm -rf {} +
FROM public.ecr.aws/lambda/python:3.12
COPY --from=builder /opt/python /opt/python
ENV PYTHONPATH=/opt/python
COPY app/ ${LAMBDA_TASK_ROOT}/
CMD ["app.handler.handler"]
# handler.py - lazy imports (importuj ciężkie biblioteki w handlerze, nie globalnie)
import json
import os
# ✅ Lekkie importy globalne
import boto3
# ❌ NIE importuj globalnie ciężkich bibliotek
# import pandas as pd # +3s do cold start
# import numpy as np # +2s do cold start
def handler(event, context):
# ✅ Lazy import - tylko gdy potrzebne
if event["path"] == "/reports":
import pandas as pd # Importowane tylko dla tego endpointu
# ...
return {
"statusCode": 200,
"body": json.dumps({"status": "ok"})
}
C) SnapStart (Java) - eliminacja zimnego startu JVM:
# SAM template - włączenie SnapStart dla Java Lambda
Resources:
MyApiFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: java21
Handler: com.example.Handler::handleRequest
MemorySize: 1024
SnapStart:
ApplyOn: PublishedVersions # Kluczowe!
AutoPublishAlias: live
Events:
Api:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
D) Warmer (tanie rozwiązanie dla niskiego ruchu):
# CloudWatch EventBridge rule - "ping" Lambda co 5 minut
Resources:
WarmingRule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: rate(5 minutes)
State: ENABLED
Targets:
- Id: keep-warm
Arn: !GetAtt MyApiFunction.Arn
Input: '{"httpMethod":"GET","path":"/warmup","isWarmer":true}'
WarmingPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref MyApiFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt WarmingRule.Arn
# W handlerze - wykryj warming request i odpowiedz natychmiast
def handler(event, context):
if event.get("isWarmer"):
return {"statusCode": 200, "body": "warm"}
# ... normalna logika
E) Zwiększ pamięć (więcej CPU = szybsza inicjalizacja):
# Lambda memory = proporcjonalnie więcej CPU
# 128 MB = 1/10 vCPU → wolna inicjalizacja
# 1024 MB = ~0.6 vCPU → szybka inicjalizacja
# 1769 MB = 1 vCPU → optymalna dla compute-heavy init
# Testuj różne wartości memory z Lambda Power Tuning:
aws lambda update-function-configuration \
--function-name my-api-function \
--memory-size 1024
# Koszt: 1024MB Lambda kosztuje 8x więcej per ms niż 128MB
# ALE: init time spada 3-5x, więc łączny koszt może być NIŻSZY
Walidacja
# 1. Sprawdź Init Duration po optymalizacji
aws logs filter-log-events \
--log-group-name /aws/lambda/my-api-function \
--start-time $(date -u -d '1 hour ago' +%s)000 \
--filter-pattern "Init Duration" \
--max-items 5 \
--query 'events[].message'
# Expected: Init Duration < 5000ms (przed optymalizacją było 10-30s)
# Jeśli provisioned concurrency: BRAK Init Duration w logach = sukces
# 2. Test cold start z wymuszeniem (deploy nowej wersji)
aws lambda update-function-configuration \
--function-name my-api-function \
--environment Variables={FORCE_COLD=$(date +%s)}
sleep 5
time curl -s https://my-api.execute-api.eu-west-1.amazonaws.com/prod/health
# Expected: response time < 5s (przed fix: 15-30s lub timeout)
# 3. Sprawdź metryki API Gateway - brak 504
aws cloudwatch get-metric-statistics \
--namespace AWS/ApiGateway \
--metric-name 5XXError \
--dimensions Name=ApiName,Value=my-api \
--start-time $(date -u -d '6 hours ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics Sum
# Expected: Sum = 0 we wszystkich okresach
# 4. Provisioned concurrency status
aws lambda get-provisioned-concurrency-config \
--function-name my-api-function \
--qualifier live
# Expected: Status = "Ready", Available = Requested
# 5. Symuluj spike ruchu (k6 / artillery)
# Sprawdź czy autoscaling provisioned concurrency skaluje w górę
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name ProvisionedConcurrencySpilloverInvocations \
--dimensions Name=FunctionName,Value=my-api-function \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Sum
# Expected: 0 (spillover = zapytania trafiające na cold instances)
Zimny start Lambda powoduje timeouty?
Umów bezpłatną 30-minutową rozmowę. Zoptymalizujemy Twoją architekturę serverless - od rozmiaru package po provisioned concurrency - aby wyeliminować 504 i obniżyć koszty.