AWS Lambda Performans Optimizasyonu: 10ms Altı Gecikme
Runtime seçimi, connection reuse, bundle disiplini, caching ve memory ayarıyla AWS Lambda warm path gecikmesini 10 ms bütçesinin içinde tutun.
Latency-kritik Lambda iş yükleri warm path’te kazanılır ya da kaybedilir. Dikkat çeken şey cold start olsa da, bir isteği tek haneli milisaniyede yanıtlayan fonksiyon aslında üç sorunun cevabıdır: handler’ı hangi runtime yüklüyor, invocation başına kaç network handshake yapılıyor ve fonksiyona ne kadar memory verilmiş.
Başlangıç için varsayılan: hot path’te derlenen bir runtime, handler dışında oluşturulan client’lar, arkada DynamoDB veya ElastiCache ve tahminle değil AWS Lambda Power Tuning ile seçilen memory. Node.js de aynı aralığa inebilir; ama bunun için bundle disiplini çok daha sıkı olmalı.
Milisaniyeler Nereye Gidiyor#
Saniyede binlerce kez yanıt vermesi gereken bir fiyatlama veya risk endpoint’i düşün. Ekip bunu, hâlihazırda tek haneli milisaniyede yanıt veren bir on-premises servisten taşıyor; yani serverless ancak aynı aralığa inerse başarı sayılır. Bu kısıt, hangi varsayılanların kabul edilebilir olduğunu değiştirir.
İlk Lambda implementasyonu genelde dört yerde zaman kaybeder ve bunlardan yalnızca biri senin yazdığın koddur:
- Init fazı: handler çalışmadan önce runtime’ın parse edip değerlendirdiği her şey; paket boyutuyla birlikte büyür
- Invocation başına handshake: her istekte veritabanına yeni bir TCP, TLS ve auth turu
- Runtime maliyeti: yorumlanan bir runtime, statik binary’nin ödemediği initialization maliyetini öder
- Yetersiz memory: Lambda CPU’yu memory ile ölçekler; düşük memory ayarı hesaplamayı kısar
Her birinin ayrı bir çözümü var ve etkileri bu sırayla birikir.
Runtime Seçimi#
Runtime’a Göre Cold Start Profilleri#
Runtime seçimi ilk olarak, kod çalışmadan önce ne kadar iş yapıldığında görünür. Açık kaynaklı lambda-perf projesi tam da bu işi düzenli olarak ölçüyor. README’de anlatılan yönteme göre her fonksiyon her gün S3’ten sıfırdan çekiliyor, deploy ediliyor ve cold start olarak on kez çağrılıyor; REPORT log satırındaki Init Duration değerleri de depoya işlenen günlük bir JSON dosyasında toplanıyor. Yayımlanan matris arm64 ve x86_64 üzerinde 128 MB ile 1.024 MB arasını kapsıyor.
13 Ağustos 2026 tarihli lambda-perf dosyasında 1.024 MB ve arm64 için on init süresinin ortalaması şöyle:
| Runtime | Init süresi, 10 cold start ortalaması | Temel takas |
|---|---|---|
provided.al2023 üzerinde Rust | 13,87 ms (12,28-14,72) | Yazması en yavaş; AWS glue kodu için en dar ekosistem |
provided.al2023 üzerinde Go | 42,00 ms (35,41-45,88) | Tek statik binary; goroutine’ler paralel I/O’yu ucuzlatır |
| Python 3.13 | 76,85 ms (63,87-85,63) | Küçük bağımlılıklar hızlı kalır; büyük wheel’ler init’i domine eder |
| Node.js 24 | 112,12 ms (93,53-129,86) | En geniş ekosistem; init maliyeti bundle boyutunu neredeyse doğrusal izler |
| .NET 10, yönetilen | 228,14 ms | Native AOT satırı değiştirir: aynı dosyada dotnet10 AOT 62,82 ms |
| Java 21, yönetilen | 243,96 ms | SnapStart tam da bu farkı kapatmak için var |
Bu tablo kısa listeye dönüşmeden önce dört kayıt düşmek gerekiyor.
Ölçülen iş yükü bir hello-world handler’ı; yani bu değerler uygulamanın değil, runtime’ın taban süresi. Paketinin INIT sırasında parse ettiği her şey bunların üstüne biner. Sonucu runtime etiketinden çok bundle boyutunun oynatmasının sebebi de bu.
Warm execution tabloda yok, çünkü veri setinde de yok. O benchmark’ta her çağrı tasarım gereği cold start; yani dosya init fazını ölçüyor, kararlı hâle hiç ulaşmıyor. Bu runtime’ları warm path’te sıralamak başka bir ölçüm ister: aynı handler’ı tekrar kullanılan bir execution environment içine arka arkaya çağırmak ve REPORT satırından Init Duration yerine Duration okumak.
Mutlak değerler oynuyor, sıralama oynamıyor. 15 ve 30 Temmuz ile 6 ve 13 Ağustos 2026 günlük dosyalarında Rust 12,83 ms ile 22,16 ms, yönetilen Java 21 ise 225,83 ms ile 270,57 ms arasında geziyor; sıralama (önce Rust, sonra Go, Python, Node.js, en sonda yönetilen Java ve .NET) dört günün hepsinde aynı kalıyor. Tarihli bir dosya kaynak gösterilebilir; bunların hiçbiri spesifikasyon değil.
Aynı günün dosyasında arm64 her satırda x86_64’ün önünde: Rust 16,12 ms, Go 49,73 ms, Python 3.13 85,14 ms, Node.js 24 147,38 ms, Java 21 263,25 ms. Benchmark’ın sitesi bölgeyi us-east-1 olarak etiketliyor; depo deploy bölgesini GitHub Actions secret’ında tuttuğu için bölgeye dair tek beyan da bu etiket.
Veri setinde hiç görünmeyen bir hafifletme yolu var, çünkü lambda-perf SnapStart ölçümü yayımlamıyor. AWS kendi belgesinde SnapStart’ın başlatma gecikmesini birkaç saniyeden, uygun senaryolarda saniyenin altına indirdiğini söylüyor; kapsamı da artık yalnızca JVM değil, Java 11 ve sonrası, Python 3.12 ve sonrası ile .NET 8 ve sonrası. Aynı sayfa provisioned concurrency’yi de fonksiyonları başlatılmış ve çift haneli milisaniyede yanıt verir durumda tutmak diye tarif ediyor; yani bu, init fazını kısaltmanın değil atlamanın bedeli.
Hot path için varsayılan Go. Aynı dosyada init tabanı Rust’ınkinin yaklaşık üç katı, yönetilen Java 21’inkinin ise yaklaşık altıda biri; üstelik karma bir ekibin gerçekten çalışabileceği bir AWS SDK ve işe alım havuzuyla birlikte geliyor:
// Go'nun concurrency modeli Lambda için mükemmel
func handler(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
start := time.Now()
// Parallel I/O operasyonları - Go'nun parladığı yer burası
var wg sync.WaitGroup
results := make(chan Result, 3)
// User verisi çek
wg.Add(1)
go func() {
defer wg.Done()
user, err := fetchUser(ctx, event.PathParameters["userID"])
results <- Result{Data: user, Err: err, Source: "user"}
}()
// Cache'den çek
wg.Add(1)
go func() {
defer wg.Done()
cached, err := getFromCache(ctx, "portfolio:"+event.PathParameters["userID"])
results <- Result{Data: cached, Err: err, Source: "cache"}
}()
// Market verisi çek
wg.Add(1)
go func() {
defer wg.Done()
market, err := getMarketData(ctx)
results <- Result{Data: market, Err: err, Source: "market"}
}()
// Timeout koruması ile sonuçları topla
go func() {
wg.Wait()
close(results)
}()
response := buildResponse(results)
// Warm execution'ı üç çağrının toplamı değil, en yavaşı belirler
log.Printf("Toplam execution: %v", time.Since(start))
return response, nil
}
Bir hot path’i Node.js’ten Go’ya taşımak faturaya iki yerden yansıyabilir: faturalanan sürede ve fonksiyonun aynı gecikme hedefine ulaşmak için ihtiyaç duyduğu memory’de. İkisini de öncesi ve sonrasıyla ölç; ekiplerin hesaba katmayı unuttuğu ikincisidir.
Veritabanı Optimizasyonu#
Invocation’lar Arası Connection Kullanımı#
En yaygın hata Lambda fonksiyonlarını geleneksel web server’lar gibi görmek. Bu yaklaşımda her invocation yeni bir veritabanı bağlantısı kurar:
// Bad: her invocation'da yeniden handshake
export const handler = async (event) => {
// Her seferinde yeni connection = istek başına TCP + TLS + auth turu
const db = await createConnection({
host: process.env.DB_HOST,
// ... connection config
});
const result = await db.query('SELECT * FROM trades WHERE id = ?', [event.id]);
await db.close(); // Connection kapatmak = israf
return { statusCode: 200, body: JSON.stringify(result) };
};
Çözüm, connection initialization’ını handler dışına taşımak:
// Good: warm invocation'lar arasında connection tekrar kullanımı
import mysql from 'mysql2/promise';
// Connection'ı handler dışında initialize et - invocation'lar arası tekrar kullanılır
let connection: mysql.Connection;
const getConnection = async () => {
if (!connection) {
connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// mysql2 bunları enableKeepAlive ve connectTimeout olarak adlandırır;
// keepAlive, acquireTimeout ve timeout burada sessizce yok sayılır
enableKeepAlive: true,
keepAliveInitialDelay: 0,
connectTimeout: 1000 // Hızlı fail: yavaş bir connect gecikmeye dönüşmemeli
});
}
return connection;
};
export const handler = async (event) => {
const start = Date.now();
try {
const db = await getConnection();
const result = await db.execute('SELECT * FROM trades WHERE id = ?', [event.id]);
console.log(`Query ${Date.now() - start}ms'de execute edildi`);
return { statusCode: 200, body: JSON.stringify(result) };
} catch (error) {
// Connection retry mantığı burada
return { statusCode: 500, body: 'Database error' };
}
};
Handshake artık her istekte değil, execution environment başına bir kez oluyor; warm path’te geriye query’nin kendisi ve bir network hop kalıyor. Takas ise canlılık: tekrar kullanılan bir connection invocation’lar arasında bayatlayabilir. Ya kullanmadan önce kontrol et ya da önüne RDS Proxy koyup pool’u ona bırak.
Veritabanı Seçimi#
Latency-kritik bir read path için kısa liste üç AWS seçeneğinden oluşuyor; bunları dürüstçe karşılaştırmanın yolu da AWS’nin reklamına değil, ölçümüne bakmak:
| Seçenek | AWS’nin belgelediği ya da ölçtüğü | Güçlü yanı | Ödediğin bedel |
|---|---|---|---|
| DynamoDB | Tek öğeli okumalarda tek haneli milisaniye ortalama, ağ hariç | Connection yönetimi SDK tarafında, VPC olmadan erişilebilir | Sınırlı query şekilleri; varsayılan eventual consistency |
| Aurora Serverless v2 | RDS Proxy path’te olduğunda query başına düşük tek haneli milisaniye ekliyor | Full SQL, ACID, tanıdık tooling | Connection yönetimi, VPC bağlantısı, proxy eklenirse query başına bir ek network hop |
| ElastiCache | AWS’nin kendi ölçek testinde p50 GET yaklaşık 751 mikrosaniye | Üçü içinde en yüksek throughput | Cache invalidation ve fonksiyonu VPC’ye sokması |
Bu hücrelerin her birinde, 10 ms bütçesiyle karşılaşınca belirleyici olan bir kayıt var.
DynamoDB’nin sayısı slogan değil, belgelenmiş bir taahhüt; ama göründüğünden dar. DynamoDB gecikme sorun giderme kılavuzu, tek haneli milisaniyelik Average SuccessfulRequestLatency değerini singleton operasyonlarla, yani primary key’i tam verilmiş tek bir öğeye yapılan işlemlerle sınırlıyor ve metriğin yalnızca servis içi gecikmeyi kapsadığını, istemci tarafındaki işi ve ağ tur süresini içermediğini söylüyor. Kuyruk tarafı da açık. AWS’nin Global Payments için yazdığı request hedging incelemesi, hedging yokken GetItem çağrısının istemci tarafı p99 değerini 9,5 ms veriyor; ikinci istek p80 eşiğinde tetiklendiğinde bu 6,7 ms’ye iniyor. %29’luk iyileşmenin bedeli %8 mükerrer istek. Eşik p50’ye çekilince iyileşme %26’da kalırken mükerrer istek %27’ye çıkıyor. Aynı yazı bölgeyi, öğe boyutunu ve yük üreticisini belirtmiyor ve simüle edilmiş bir ortamdan söz ediyor; yani 9,5 ms plan yapılacak bir sayı değil, bir büyüklük mertebesi.
İkisini yan yana koyunca bütçe aritmetiği rahatsız edici hâle geliyor: DynamoDB’ye dayanan bir path’te, handler daha hiçbir şey yapmadan veritabanının kendi kuyruğu 10 ms’lik bütçenin büyük kısmını harcayabiliyor. Alarmın biçimini de bu belirliyor. 10 ms eşiği p95 üzerinden anlamlı, çünkü p99’da bütçeyi tek başına veritabanı bitirebilir.
Pazarlamayla ölçümün bir büyüklük mertebesi ayrıştığı yer ElastiCache. ElastiCache ürün sayfası mikrosaniye gecikmesinden söz ediyor. AWS’nin ElastiCache Serverless duyurusundaki kendi ölçümü ise saniyede 1 milyon isteğe ölçeklenirken, %80 okuma ve %20 yazma karışımıyla ve 512 baytlık değerlerle p50 GET gecikmesini yaklaşık 751 mikrosaniye, her zaman 860 mikrosaniyenin altında raporluyor; p50 SET ise yaklaşık 1.050 mikrosaniye ve 1.200 mikrosaniyenin altında. İkisi de doğru olabilir, çünkü farklı şeyleri ölçüyorlar. AWS’nin ElastiCache for Valkey’de sunucu tarafı gecikmeyi izleme rehberi, yalnızca ön işleme, komut çalıştırma ve son işlemeyi kapsayan engine metriğini istemcinin gördüğü süreden ayırıyor; ikincisine istemci kaynakları ve ağ da dahil. Rehberin teşhisi de bu ayrımdan çıkıyor: istemci tarafındaki gecikme yükselirken sunucu metriği sabit kalıyorsa sorumlu büyük ihtimalle engine değildir. Bütçe yaparken doğru referans milisaniyenin altı; mikrosaniye tur süresini değil, engine’i tarif ediyor.
Aurora Serverless v2 genelde gecikme için değil query şekli için seçilir. VPC içindeki bir Lambda fonksiyonu ona doğrudan bağlanabilir; RDS Proxy de zorunlu bir bileşen olmaktan çok, connection yönetimi için bir seçenek. Eşzamanlılık ve kısa ömürlü bağlantılar veritabanının bağlantı bütçesini zorlamaya başladığında yerini hak eder. Bunu önce kendi iş yükünde ölç, çünkü proxy hop’unun belgelenmiş bir bedeli var. AWS’nin RDS Proxy rehberi tipik olarak düşük tek haneli milisaniyelik bir ek gecikme gözlendiğini söylüyor ve bunu fark eden iş yükü sınıfını da adıyla anıyor: tek haneli milisaniye ya da milisaniye altı query çalıştıran uygulamalar, ek network hop query’nin kendisinin yanında büyük kaldığı için bunu hisseder.
Varsayılan: primary data için DynamoDB, önüne ElastiCache ise ancak read path bir VPC bağlantısını hak edecek kadar sıcaksa. Ekiplerin atladığı takas tam olarak bu son cümlecik. DynamoDB public AWS endpoint üzerinden erişilebilir, ElastiCache değil; yani cache eklemek fonksiyona VPC networking’i ve ENI yaşam döngüsünü de ekler. Sıcak path özellikle DynamoDB okumalarıysa DAX aynı bahsin dar versiyonu: AWS bunun için milisaniyeden mikrosaniyeye, 10 kata varan bir iyileşme belgeliyor, VPC şartı ise aynı kalıyor.
Optimize edilmiş bir DynamoDB okuması şöyle görünür:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
// Client'ı handler dışında initialize et
const client = new DynamoDBClient({
region: process.env.AWS_REGION,
maxAttempts: 2, // Düşük latency için hızlı fail
});
const docClient = DynamoDBDocumentClient.from(client, {
marshallOptions: {
removeUndefinedValues: true,
},
});
export const getTradeData = async (tradeId: string) => {
const start = Date.now();
try {
const response = await docClient.send(
new GetCommand({
TableName: "Trades",
Key: { tradeId },
ConsistentRead: true // İki katı read capacity, üstüne bir de gecikme
})
);
const latency = Date.now() - start;
console.log(`DynamoDB read: ${latency}ms`);
return response.Item;
} catch (error) {
console.error(`DynamoDB error ${Date.now() - start}ms sonra:`, error);
throw error;
}
};
Bundle Boyutu ve Init Fazı#
Bundle boyutu Lambda’da süs bir metrik değil. Paketteki her şey, handler çağrılmadan önce INIT sırasında okunur, parse edilir ve değerlendirilir. Bu yüzden birkaç megabaytlık bir Node.js bundle’ının bedeli her cold start’ta ödenir.
ESBuild Konfigürasyonu#
Lambda bundling’i için pratik varsayılan esbuild: her build’de çalışacak kadar hızlı ve external davranışı yönetilen runtime’ın zaten sağladığı paketlerle temiz eşleşiyor.
// esbuild.config.js
const esbuild = require('esbuild');
const config = {
entryPoints: ['src/index.ts'],
bundle: true,
minify: true,
target: 'node20',
format: 'esm', // Daha iyi tree-shaking için ES module'ler
platform: 'node',
outfile: 'dist/index.mjs',
// Kritik optimizasyonlar
external: [
'@aws-sdk/*', // AWS SDK v3'ü yönetilen runtime sağlasın
'aws-sdk' // v2'nin desteği bitti; asla gönderme
],
treeShaking: true,
mainFields: ['module', 'main'], // ES module'leri tercih et
// Çıktı boyutlarını okunur kılan şey metafile: write: false
// ayarlamadıkça result.outputFiles boş kalır
metafile: true,
sourcemap: 'external',
};
esbuild.build(config)
.then((result) => {
const [, output] = Object.entries(result.metafile.outputs)
.find(([file]) => file.endsWith('.mjs'));
console.log(`Bundle boyutu: ${(output.bytes / 1024).toFixed(2)}KB`);
// Bundle Lambda'ya ulaşmadan build'i düşür
if (output.bytes > 500 * 1024) { // 500KB limit
throw new Error(`Bundle çok büyük: ${(output.bytes / 1024).toFixed(2)}KB`);
}
})
.catch((error) => {
console.error(error);
process.exit(1);
});
@aws-sdk/*’yi external işaretlemek bundle’ı küçük tutar ama SDK sürümlemesini runtime’a devreder. Deployment’lar arasında tekrarlanabilir bir SDK sürümü gerekiyorsa SDK’yı bundle’a dahil et ve fazladan kilobyte’ları kabul et.
AWS SDK v3: Modüler Mimari Faydaları#
SDK v3 monolitik paketi servis başına client’lara böler; böylece bundle sadece yaptığın çağrıları taşır:
// Bad: v2 tüm SDK yüzeyini çeker ve desteği sona erdi
import AWS from 'aws-sdk';
const dynamodb = new AWS.DynamoDB.DocumentClient();
// Good: Yeni yol - sadece ihtiyacın olanı import et
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
Bundle çalışmasının karşılığı INIT fazında görünür; CloudWatch Logs’taki REPORT satırı bunu ayrı bir alan olarak Init Duration ile kaydeder. Bu alanı öncesi ve sonrasıyla karşılaştır: bundle boyutunun kontrol ettiği tek kısım orası.
Caching Stratejisi#
ElastiCache sıcak read path’in önünde durur. Önemli olan yapı singleton: execution environment başına tek client, module scope’ta oluşturulur, asla handler’ın içinde değil.
import Redis from 'ioredis';
// Connection singleton - performans için kritik
let redis: Redis | null = null;
const getRedisConnection = (): Redis => {
if (!redis) {
redis = new Redis({
host: process.env.REDIS_ENDPOINT,
port: 6379,
// Hızlı fail: yavaş bir cache gecikmenin kendisine dönüşmemeli
connectTimeout: 1000,
commandTimeout: 500,
maxRetriesPerRequest: 2, // Sonsuza kadar retry yapma
keepAlive: 30000, // Connection'ları canlı tut
lazyConnect: true, // İlk kullanımda bağlan
family: 4, // IPv4 kullan
db: 0,
});
// Monitoring için connection event logging
redis.on('connect', () => console.log('Redis bağlandı'));
redis.on('error', (err) => console.error('Redis error:', err));
}
return redis;
};
// Performance monitoring ile cache-aside pattern
export const getCachedData = async (key: string, ttl = 300): Promise<any> => {
const start = Date.now();
try {
const cached = await getRedisConnection().get(key);
const cacheLatency = Date.now() - start;
console.log(`Cache lookup: ${cacheLatency}ms`);
if (cached) {
// Cache hit - tek bir ağ turu, veritabanı çağrısı yok
return JSON.parse(cached);
}
// Cache miss - veritabanından getir
const data = await fetchFromDatabase(key);
// Response'u bloke etmemek için cache'i asenkron set et
getRedisConnection()
.setex(key, ttl, JSON.stringify(data))
.catch(err => console.error('Cache set error:', err));
return data;
} catch (error) {
const errorLatency = Date.now() - start;
console.error(`Cache error ${errorLatency}ms sonra:`, error);
// Cache failure'da veritabanına fallback
return await fetchFromDatabase(key);
}
};
// Yüksek performanslı batch operasyonlar
export const batchGetCached = async (keys: string[]): Promise<Record<string, any>> => {
const start = Date.now();
try {
const results = await getRedisConnection().mget(...keys);
console.log(`Batch cache lookup (${keys.length} key): ${Date.now() - start}ms`);
const parsed: Record<string, any> = {};
keys.forEach((key, index) => {
if (results[index]) {
parsed[key] = JSON.parse(results[index]);
}
});
return parsed;
} catch (error) {
console.error(`Batch cache error:`, error);
return {};
}
};
Client ayarlarından daha önemli iki şey var. Birincisi, connection invocation’lar arasında hayatta kalmalı. Kalmazsa her okuma yeni bir TCP turu öder ve cache, korumaya çalıştığı veritabanından yavaş olur. İkincisi, cache okuması da ağdan geçer; yani alt sınır Redis’in kendisi değil, VPC tur süresidir.
ElastiCache Konfigürasyonu#
Fonksiyonla aynı subnet’lere yerleştirilmiş minimal bir cluster tanımı:
# Redis kurulumu için CloudFormation şablonu
ElastiCacheSubnetGroup:
Type: AWS::ElastiCache::SubnetGroup
Properties:
Description: Subnet group for Lambda Redis access
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
ElastiCacheCluster:
Type: AWS::ElastiCache::CacheCluster
Properties:
CacheNodeType: cache.r6g.large # Memory optimized
Engine: redis
EngineVersion: 7.0
NumCacheNodes: 1
VpcSecurityGroupIds:
- !Ref RedisSecurityGroup
CacheSubnetGroupName: !Ref ElastiCacheSubnetGroup
# Performans optimizasyonları
PreferredMaintenanceWindow: sun:03:00-sun:04:00
SnapshotRetentionLimit: 1
SnapshotWindow: 02:00-03:00
Memory ve CPU#
Lambda CPU’yu memory ile orantılı dağıtır. Fonksiyon 1.769 MB’ta tam bir vCPU eşdeğerine ulaşır; altında çekirdeğin bir kesriyle çalışır. CPU-bound bir handler’da memory’yi artırmak bir memory kararı değildir. Lambda’nın açtığı tek CPU ayarıdır.
Bu da alışılmış maliyet sezgisini tersine çevirir. us-east-1’de x86 üzerinde ayda 1M invocation düşün; AWS Lambda fiyatlandırma sayfası milyon istek başına 0,20 USD ve GB-saniye başına 0,0000166667 USD veriyor, ücretsiz katmanı hesap dışında tutuyoruz:
- 512 MB, invocation başına faturalanan 20 ms: 0,5 GB × 0,02 sn × 1M = 10.000 GB-saniye, yani 0,17 USD süre artı 0,20 USD istek, toplam 0,37 USD
- 1.024 MB, invocation başına faturalanan 10 ms: 1 GB × 0,01 sn × 1M = 10.000 GB-saniye, yine toplam 0,37 USD
Memory’yi ikiye katlamak, süreyi yarıya indirdiği her durumda bedavadır; süre yarıdan fazla düşüyorsa net tasarruftur. Bu başabaş noktasının altında fazladan memory gerçekten para götürür. Peşine düşülmesi gereken sayı da bu başabaş noktası. Başkasının yayımladığı bir memory ayarı senin handler’ına taşınmaz.
AWS Lambda Power Tuning#
Power Tuning bu başabaş noktasını, genel tahminler yerine belirli bir fonksiyon üzerinde ölçerek bulur. Serverless Application Repository uygulaması olarak dağıtılır ve bir Step Functions state machine kurar; yani Lambda çağırmak yerine execution başlatırsın:
# Serverless Application Repository'den bir kez deploy et, sonra:
aws stepfunctions start-execution \
--state-machine-arn arn:aws:states:us-east-1:123456789012:stateMachine:powerTuningStateMachine \
--input '{
"lambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
"powerValues": [128, 256, 512, 1024, 1536, 2048],
"num": 50,
"payload": {"test": "data"},
"parallelInvocation": true,
"strategy": "cost"
}'
Üzerinde düşünmeye değen kısım strategy. cost faturayı, speed gecikmeyi optimize eder, balanced ikisini ortalar. Gecikme bütçesi varsa speed ile çalıştır, sonra kazanan power değerinin yukarıdaki maliyet başabaş noktasının içinde kalıp kalmadığına bak.
VPC Networking#
Lambda fonksiyonlarını VPC dışında tutma tavsiyesi Eylül 2019 öncesine ait. O dönemde her fonksiyon kendi ENI’sini bağlıyordu ve VPC içindeki bir cold start on saniyeyi bulabiliyordu. AWS bu modeli, subnet ve security group kombinasyonu başına bir kez oluşturulan paylaşımlı Hyperplane ENI’leriyle değiştirdi; fonksiyon başına bağlanma maliyeti de ortadan kalktı.
Geriye kalan sıfır değil. VPC’ye bağlı bir fonksiyon, NAT gateway veya VPC endpoint olmadan public internete hâlâ çıkamaz ve yeni bir subnet ile security group çiftindeki ilk fonksiyon ENI oluşturmayı hâlâ bekler. Ama VPC bağlantısı artık latency-hassas bir path’te ElastiCache veya RDS’ten kaçınmak için gerekçe değil.
HTTP Keep-Alive#
Her AWS SDK çağrısı bir HTTPS isteğidir ve connection reuse olmadan her biri TLS handshake öder. AWS SDK for JavaScript v3 TCP bağlantılarını varsayılan olarak tekrar kullanır; yani agent’ı açıkça yapılandırmanın sebebi socket sayısını ve timeout’ları kontrol etmektir:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { Agent } from "node:https";
// AWS endpoint'leri HTTPS; bu yüzden httpAgent değil httpsAgent olmalı
const httpsAgent = new Agent({
keepAlive: true,
maxSockets: 50
});
const dynamoClient = new DynamoDBClient({
region: process.env.AWS_REGION,
maxAttempts: 2,
requestHandler: new NodeHttpHandler({
httpsAgent,
connectionTimeout: 1000,
requestTimeout: 2000
})
});
SDK v2’de bunun karşılığı AWS_NODEJS_CONNECTION_REUSE_ENABLED=1 ortam değişkeniydi; hâlâ v2 kodu çalıştıran fonksiyonlarda kontrol etmeye değer.
Tekrar kullanım ancak ortada bir bağlantı kaldığı sürece işe yarar. AWS’nin DynamoDB gecikme sorun giderme kılavuzu, yeni bir bağlantı üzerindeki ilk isteğin onu tekrar kullanan isteklerden yavaş olduğunu belirtiyor. Önerdiği çözüm de başka istek yokken 30 saniyede bir keep-alive amaçlı GetItem göndermek. Bu öneri, istekler arasında çalışmaya devam eden bir istemciyi varsayar; Lambda fonksiyonu böyle bir istemci değil. Lambda, invocation döner dönmez execution environment’ı dondurur; path boştayken fonksiyonun içinde hiçbir şey zamanlayıcıyla tetiklenmez. Zamanlanmış bir warmer da fikri kurtarmaz: istek maliyeti ekler ve bağlantısını korumak istediğin execution environment’ı seçemez. İki yoğunluk arasında sessizleşen bir path’te aradan sonraki ilk istek handshake’i öder; gecikme bütçesinin buna yer bırakması gerekir.
Monitoring ve Alerting#
Custom CloudWatch Metrikleri#
Yerleşik Duration metriği tüm invocation’ı kapsar; bu da yavaş bir query ile yavaş bir cold start’ı ayırt etmeye yetmez. Custom dimension’lar bu boşluğu kapatır:
import { CloudWatch } from '@aws-sdk/client-cloudwatch';
const cloudwatch = new CloudWatch({});
export const trackPerformanceMetrics = async (
functionName: string,
operationType: string,
duration: number,
cacheHit: boolean,
success: boolean
) => {
const metrics = [
{
MetricName: 'ResponseTime',
Value: duration,
Unit: 'Milliseconds',
Dimensions: [
{ Name: 'FunctionName', Value: functionName },
{ Name: 'OperationType', Value: operationType },
{ Name: 'Success', Value: success.toString() }
]
},
{
MetricName: 'CacheHitRate',
Value: cacheHit ? 1 : 0,
Unit: 'Count',
Dimensions: [
{ Name: 'FunctionName', Value: functionName },
{ Name: 'OperationType', Value: operationType }
]
}
];
await cloudwatch.putMetricData({
Namespace: 'Lambda/Performance',
MetricData: metrics
});
};
// Lambda fonksiyonunda kullanım
export const handler = async (event, context) => {
const start = Date.now();
let cacheHit = false;
let success = false;
try {
// Fonksiyon logic'in burada
const { payload, servedFromCache } = await processRequest(event);
cacheHit = servedFromCache;
success = true;
return { statusCode: 200, body: JSON.stringify(payload) };
} catch (error) {
console.error('Function error:', error);
return { statusCode: 500, body: 'Internal error' };
} finally {
const duration = Date.now() - start;
// Metrikleri asenkron takip et
trackPerformanceMetrics(
context.functionName,
event.operationType || 'default',
duration,
cacheHit,
success
).catch(err => console.error('Metrics error:', err));
}
};
Bir uyarı: PutMetricData da bir ağ çağrısı. Tek haneli milisaniye bütçesi olan bir fonksiyonda CloudWatch Embedded Metric Format’ı tercih et; stdout’a yapılandırılmış JSON yazar ve metrikleri log pipeline çıkarır. Aynı dimension’lar, istek path’inde API çağrısı yok.
10 ms Gecikme Bütçesi için CloudWatch Alarmları#
Aşağıdaki alarm Duration metriğini okur; yani yalnızca fonksiyonun kendi çalışma süresini kapsar. API Gateway’in işleme süresi, istemciyle edge arasındaki ağ ve geri dönen yanıt bunun dışında kalır. API Gateway Latency metriği bir sonraki bileşeni ekler: gateway isteği aldığı anda başlar, yanıtı döndürdüğü anda biter. Bu da edge’de alınan sunucu tarafı bir ölçüm; yani uzaktaki bir istemcinin p95’i 10 ms bütçesinin dışına çıkarken iki metrik de eşiğin altında kalabilir. İstemcinin gerçekten gördüğü sayı, endpoint’e bakan bir synthetic canary’den ya da istemcinin kendi içindeki gerçek kullanıcı ölçümünden gelir.
# CloudWatch alarm konfigürasyonu
HighLatencyAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${FunctionName}-High-P95-Latency"
AlarmDescription: "Lambda P95 latency exceeded 10ms"
MetricName: Duration
Namespace: AWS/Lambda
ExtendedStatistic: p95 # Statistic: Average kuyruğu gizler
Period: 60
EvaluationPeriods: 2
Threshold: 10 # 10ms eşiği
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: !Ref LambdaFunction
AlarmActions:
- !Ref PerformanceAlertTopic
# Performans monitoring için özel dashboard
PerformanceDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: !Sub "${FunctionName}-Performance"
DashboardBody: !Sub |
{
"widgets": [
{
"type": "metric",
"properties": {
"metrics": [
[ "Lambda/Performance", "ResponseTime", "FunctionName", "${FunctionName}" ]
],
"period": 60,
"stat": "p95",
"region": "${AWS::Region}",
"title": "Response Time (P95)"
}
}
]
}
Yaygın Tuzaklar#
Bağımlılıklar Üzerinden Bundle Regresyonu#
Bundle boyutu tek seferlik bir düzeltme değil. Bir kütüphanenin CommonJS build’ini import eden tek bir bağımlılık, o kütüphanenin tamamı için tree-shaking’i devre dışı bırakır; otomatik bir dependency güncellemesi de bunu kimse diff’i okumadan geri getirebilir.
Temel neden pattern’i: lodash-es yerine lodash eklenmesi tüm utility kütüphanesini çeker.
Çözüm: CI/CD pipeline’ında bundle boyut kontrolleri:
# GitHub Actions workflow check
- name: Bundle boyutu kontrol et
run: |
BUNDLE_SIZE=$(stat -c%s "dist/index.mjs")
BUNDLE_SIZE_KB=$((BUNDLE_SIZE / 1024))
echo "Bundle boyutu: ${BUNDLE_SIZE_KB}KB"
if [ $BUNDLE_SIZE_KB -gt 500 ]; then
echo "Bundle çok büyük: ${BUNDLE_SIZE_KB}KB > 500KB limit"
exit 1
fi
Redis Connection Yaşam Döngüsü#
Hit oranı yüksek bir cache, her invocation yeni bağlantı açıyorsa yine de yavaş olur. Belirti şu: sağlıklı görünen bir hit oranının yanında veritabanı gibi duran bir cache gecikmesi.
Singleton’ı iki şey bozar. Handler içinde oluşturulan bir client zaten her seferinde yenidir. Daha az fark edileni ise client’ı disconnect eden bir process.on('beforeExit') handler’ı: invocation sonrası event loop boşaldığında çalışır ve tam olarak bir sonraki invocation’ın tekrar kullanacağı bağlantıyı kapatır.
Çözüm: Yalnızca client gerçekten kullanılamaz durumdaysa yeniden bağlan:
// Module scope: execution environment başına bir kez oluşturulur
let redis: Redis | null = null;
const getRedisConnection = (): Redis => {
// 'end' client'ın bittiği anlamına gelir. !== 'ready' kontrolü,
// client daha bağlanırken onu yeniden kurardı.
if (!redis || redis.status === 'end') {
redis = new Redis({
// konfigürasyon
});
}
return redis;
};
Erişim Path’ine Göre Consistency#
Performansı maksimize etmek için tüm DynamoDB okumalarında eventual consistency kullanmak, bir race condition ortaya çıkana kadar işe yarar: kullanıcılar yüksek frekanslı güncellemeler sırasında bayat trade verisi görür.
Çözüm: Kritik path’ler için seçici strong consistency:
// Performans vs consistency karar matrisi
const consistencyConfig = {
userProfile: { consistentRead: false }, // Eventually consistent OK
tradeData: { consistentRead: true }, // Strong consistency required
marketData: { consistentRead: false }, // Eventually consistent OK
balances: { consistentRead: true } // Strong consistency required
};
const getTradeData = async (tradeId: string) => {
return await docClient.send(
new GetCommand({
TableName: "Trades",
Key: { tradeId },
ConsistentRead: consistencyConfig.tradeData.consistentRead
})
);
};
Nereden Başlamalı#
Sıra, listenin kendisinden daha önemli. Her adım ancak bir öncekisi tamamlandığında karşılığını verir:
- Her client’ı handler dışına taşı. Veritabanı, cache ve SDK client’ları module scope’a ait. Her invocation kendi bağlantısını açmaya devam ederken buradaki hiçbir şeyin önemi yok.
- Memory’yi seçmeden önce Power Tuning çalıştır. Tartışmayı bir sayıyla değiştirir ve genelde memory ayarını yükseltir.
- CI’a bundle boyut gate’i koy. Build’i düşüren bir eşik,
Init Duration’ın yeniden tırmanmasını engelleyen tek şey. - Runtime’ı path bazında seç. Hot path’teki bir Go fonksiyonu, geri kalan her yerdeki Node.js fonksiyonlarının yanında durabilir; seçimin repository geneline yayılması gerekmez.
- Caching’i en sona bırak. Hem en büyük kazanç hem de en büyük doğruluk hatası kaynağı; üstelik fonksiyonu VPC’ye sokar.
Tek haneli milisaniye Lambda yanıtları ulaşılabilir bir hedef ve arkasındaki iş gösterişsiz: connection’ları tekrar kullan, bundle’ı küçük tut, fonksiyona yeterli CPU ver, hak eden okumaları cache’le. Buradaki her rakam tek bir bileşeni ölçüyor; p95 Duration alarmı da öyle, yalnızca fonksiyonun kendi çalışma süresini kapsıyor. Path’in tamamı bütçeyi tutuyor mu sorusunu ise yalnızca istemci tarafında alınan bir ölçüm yanıtlayabilir. Bu varsayılan, gecikme gerçekten ürün sözleşmesinin bir parçasıysa geçerli. Değilse tersini seç: 100 ms’in sorun olmadığı bir path’te aynı efor yuvarlama hatası kadar kazandırır, provisioned concurrency artı derlenmiş runtime ise boşuna ödenen bir maliyete dönüşür.
Kaynaklar#
- lambda-perf: 13 Ağustos 2026 Tarihli Günlük Cold Start Veri Seti (yeni sekmede açılır) - Runtime tablosunun ortalamasının alındığı tarihli dosya; her runtime için on ayrı çağrının init süresini, memory boyutunu, mimariyi ve paket tipini içerir.
- lambda-perf: Depo ve Ölçüm Yöntemi (yeni sekmede açılır) - Ölçümün nasıl üretildiği: her fonksiyon her gün S3’ten sıfırdan çekilir, deploy edilir ve cold start olarak on kez çağrılır; Init Duration değeri REPORT log satırından okunur.
- Amazon DynamoDB’de Gecikme Sorunlarını Giderme (yeni sekmede açılır) - AWS’nin tek haneli milisaniye rakamını tek öğeli işlemlerle sınırlayan ve ağ süresini kapsam dışında bırakan belge; 30 saniyelik keep-alive önerisi ile DAX rakamı da burada.
- Global Payments DynamoDB’de Request Hedging ile Kuyruk Gecikmesini Nasıl İyileştirdi (yeni sekmede açılır) - AWS’nin ölçtüğü istemci tarafı GetItem kuyruğu: hedging yokken p99 9,5 ms, p80 eşiğinde 6,7 ms; her ayarın mükerrer istek bedeliyle birlikte.
- Amazon ElastiCache Serverless Genel Kullanıma Açıldı (yeni sekmede açılır) - AWS’nin saniyede 1 milyon isteğe ölçeklenirken yaptığı kendi gecikme ölçümü: p50 GET yaklaşık 751 mikrosaniye, p50 SET yaklaşık 1.050 mikrosaniye.
- Amazon ElastiCache Ürün Sayfası (yeni sekmede açılır) - Mikrosaniye gecikme iddiası; AWS’nin kendi milisaniye altı ölçümlerinin yanında okumaya değer.
- Amazon ElastiCache for Valkey’de Sunucu Tarafı Gecikmeyi İzleme (yeni sekmede açılır) - Engine tarafındaki gecikmeyi istemcinin gördüğü süreden ayırır; cache okumasının alt sınırının engine değil VPC tur süresi olmasının sebebi budur.
- RDS Proxy Yaygın Kullanım Senaryoları (yeni sekmede açılır) - AWS’nin proxy hop’u için belgelediği maliyet ve bunu hisseden iş yükü sınıfı: milisaniye altı ile tek haneli milisaniye query çalıştıran uygulamalar.
- Lambda SnapStart: Başlatma Performansını İyileştirme (yeni sekmede açılır) - Saniye altı başlatma iddiası, güncel runtime kapsamı (Java 11+, Python 3.12+, .NET 8+) ve provisioned concurrency için çift haneli milisaniye rakamı.
- AWS Lambda Fiyatlandırması (yeni sekmede açılır) - Memory hesabında kullanılan istek başına ve GB-saniye başına ücretler ile 1 ms faturalama hassasiyeti.
- Lambda Çalışma Zamanı Ortamı Yaşam Döngüsü (yeni sekmede açılır) - Init Duration ve cold start davranışının arkasındaki INIT, INVOKE ve SHUTDOWN aşamalarını açıklayan resmi AWS belgesi.
- AWS Lambda Power Tuning ile Fonksiyon Profilleme (yeni sekmede açılır) - Tek bir fonksiyona ait memory ayarını bulan state machine’in kullanımına dair resmi AWS kılavuzu.
- AWS Lambda Fonksiyonları İçin İyileştirilmiş VPC Networking (yeni sekmede açılır) - Saniyeler süren VPC cold start cezasını ortadan kaldıran 2019 tarihli paylaşımlı Hyperplane ENI geçişi.
- Node.js’te Keep-Alive ile Bağlantı Tekrar Kullanımı (yeni sekmede açılır) - AWS SDK for JavaScript’in TCP bağlantılarını varsayılan olarak tekrar kullandığını doğrulayan ve agent yapılandırmasını gösteren resmi belge.
- CloudWatch Embedded Metric Format (yeni sekmede açılır) - İstek path’inde PutMetricData çağrısı yapmadan, yapılandırılmış loglar üzerinden custom metrik yayma.
İlgili yazılar
AWS Lambda'da Node.js'den Go'ya geçiş ne zaman kendini amorti eder, ne zaman etmez: karar çerçevesi, serverless Go pattern'ları ve maliyet matematiği.
go · nodejs · serverless +5
Single Table Design'da DynamoDB throttling'i önleme ve yönetme: partition key tasarımı, write sharding, kapasite modları, DAX caching ve retry pattern'leri.
dynamodb · aws · reliability +4
Global uygulamalar için AWS edge computing çözümlerini seçme ve uygulama üzerine pratik örnekler ve maliyet optimizasyonu stratejileri içeren kapsamlı teknik rehber.
aws · cloudfront · lambda +6
Çok katmanlı caching için pratik rehber: in-memory, Redis ve CDN katmanları, cache-aside ve write-through, ElastiCache ve MemoryDB, stampede önleme.
caching · redis · aws +4
DynamoDB single-table design'da ilişki modelleme, GSI ve LSI seçimi, DAX optimizasyonu ve production'da yaygın hatalardan kaçınmayı pratik örneklerle öğren.
dynamodb · data-storage-orm · aws +1