İçeriğe atla

AWS Lambda'da Bun ve Alternatif JavaScript Runtime'ları Çalıştırma

Bun ve Deno'yu AWS Lambda'da custom runtime ile çalıştırma: performans benchmark'ları, maliyet analizi ve production deployment pattern'leri.

Ayhan Sipahi Ayhan Sipahi

AWS Lambda resmi olarak Node.js’i destekliyor, ancak platformun custom runtime özelliği Bun ve Deno gibi alternatif JavaScript runtime’larına kapı açıyor. Bunu iki mekanizma mümkün kılıyor: Lambda Layer’ları ve container image’ları. İkisi de AWS’nin kendi yönetilen runtime’larına uyguladığı initialization ayarlarından vazgeçiyor ve bedeli cold start’ta ödeniyor.

Çoğu Lambda workload’ı için doğru varsayılan hâlâ yönetilen Node.js runtime’ı. Alternatif bir runtime’ın yerini hak etmesi için ölçülmüş bir kısıt gerekiyor; o noktada da cache’i önceden ısıtılmış Deno container image’ı iki yoldan daha öngörülebilir olanı.

Custom Runtime Sorusu#

Alternatif JavaScript runtime’ları birkaç durumda cazip hale geliyor: latency’ye duyarlı uygulamalarda cold start overhead’i, TypeScript transpilation adımından kurtulma, runtime verimliliğinin faturaya yansıdığı CPU-bound workload’lar ve Node.js LTS desteğinden önce modern JavaScript özelliklerine erişim.

Temel trade-off şu: AWS Lambda yönetilen runtime’ları için yoğun şekilde optimize edilmiş, custom runtime ise bu optimizasyonlardan vazgeçiyor. Kazanılan performansın hem cold start cezasını hem de implementasyon maliyetini karşılaması gerekiyor.

Lambda Custom Runtime’ları Nasıl Çalışır#

AWS Lambda’nın custom runtime özelliği, Lambda Runtime API’yi implement ederek herhangi bir runtime çalıştırmanıza olanak tanıyor. Bu API, runtime’ınızın event’leri almak ve response’ları döndürmek için kullandığı basit bir HTTP interface sağlıyor.

Runtime API Flow’u#

// Basitleştirilmiş Lambda Runtime API implementasyonu
const RUNTIME_API = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime`;

while (true) {
  // 1. Sonraki invocation'ı al
  const eventResponse = await fetch(`${RUNTIME_API}/invocation/next`);
  const requestId = eventResponse.headers.get('Lambda-Runtime-Aws-Request-Id');
  const event = await eventResponse.json();

  try {
    // 2. Handler'ı çağır
    const result = await handler(event);

    // 3. Response'u döndür
    await fetch(`${RUNTIME_API}/invocation/${requestId}/response`, {
      method: 'POST',
      body: JSON.stringify(result),
    });
  } catch (error) {
    // 4. Error'u bildir
    await fetch(`${RUNTIME_API}/invocation/${requestId}/error`, {
      method: 'POST',
      body: JSON.stringify({
        errorMessage: error.message,
        errorType: error.constructor.name,
      }),
    });
  }
}

Bootstrap süreci sonsuz döngüde çalışıyor: Lambda’dan event’leri talep ediyor, handler’ınızı çalıştırıyor ve sonuçları döndürüyor. Bu basit protokol, custom runtime’ları mümkün kılan şey.

İmplementasyon Yaklaşımı 1: Lambda Layer’ları ile Bun#

Lambda Layer’ları, runtime bağımlılıklarını paketlemenin ve birden fazla function arasında paylaşmanın bir yolunu sağlıyor. Bun, Runtime API’yi implement eden resmi bun-lambda package’ı sağlıyor.

Bun Lambda Layer’ını Build Etme#

# Bun repository'sini clone'la
git clone https://github.com/oven-sh/bun.git
cd bun/packages/bun-lambda

# Layer'ı build et ve publish et (mimari varsayılanı aarch64)
bun run publish-layer

# x86_64 için build et (uyumluluk için önerilir)
bun run publish-layer -- --arch x64

--arch bayrağı yalnızca x64 veya aarch64 değerlerini kabul ediyor; Lambda konsolunda mimarinin adı arm64 olsa da bu bayrakta geçerli bir değer değil.

Publish script’i Bun runtime’ı ve bootstrap script’i ile bir Lambda Layer oluşturuyor, sonra AWS hesabınıza publish ediyor. arn:aws:lambda:us-east-1:123456789012:layer:bun-runtime:1 gibi görünen bir Layer ARN alacaksınız.

Bun Lambda Handler Yapısı#

Bun Lambda handler’ları Node.js convention’ları yerine Web API standardını takip ediyor:

// handler.ts - Bun Lambda handler
export default {
  async fetch(request: Request): Promise<Response> {
    const event = await request.json();

    // Lambda event'ini işle
    const result = {
      message: 'Hello from Bun on Lambda!',
      timestamp: Date.now(),
      input: event,
    };

    return new Response(JSON.stringify(result), {
      headers: { 'Content-Type': 'application/json' },
    });
  },
};

Handler’ın handler değil fetch metodu export ettiğine dikkat edin. Bu Bun’ın Web API yaklaşımını takip ediyor. Lambda event’leri standart Request objelerine dönüştürülüyor ve handler’ınız Response objeleri döndürüyor.

AWS CDK ile Deploy Etme#

import { Function, Runtime, Code, LayerVersion, Architecture } from 'aws-cdk-lib/aws-lambda';

// Publish edilmiş Bun layer'ına referans ver
const bunRuntimeLayer = LayerVersion.fromLayerVersionArn(
  this,
  'BunRuntime',
  'arn:aws:lambda:us-east-1:123456789012:layer:bun-runtime:1'
);

const bunFunction = new Function(this, 'BunFunction', {
  runtime: Runtime.PROVIDED_AL2023,
  handler: 'index.fetch',
  code: Code.fromAsset('dist'),
  layers: [bunRuntimeLayer],
  architecture: Architecture.X86_64, // Layer mimarisi ile eşleşmeli
});

Kritik gereklilik: Layer mimarisi function mimarisi ile eşleşmeli. Her ikisine de ihtiyacınız varsa x86_64 ve arm64 için ayrı layer’lar build edin.

İmplementasyon Yaklaşımı 2: Container Image’ları#

Container image’ları runtime environment üzerinde tam kontrol sağlıyor ve gelişmiş optimizasyonları mümkün kılıyor. Bu yaklaşım, HTTP sunucularını Lambda-uyumlu handler’lara dönüştürmek için AWS Lambda Web Adapter kullanıyor.

Bun Container Deployment#

# Bun Lambda deployment için multi-stage build
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS aws-lambda-adapter
FROM oven/bun:1-debian AS runtime

# Lambda adapter'ı kopyala
COPY --from=aws-lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter

WORKDIR /var/task

# Dependency'leri yükle
COPY package.json bun.lock ./
RUN bun install --production --frozen-lockfile

# Uygulamayı kopyala
COPY . .

# Adapter'ın varsayılan olarak yönlendirdiği portta servis ver
ENV PORT=8080

CMD ["bun", "run", "index.ts"]

Lambda adapter gelen Lambda event’lerini intercept ediyor, bunları 8080 portundaki sunucunuza HTTP request’lere dönüştürüyor, sonra response’ları Lambda formatına geri dönüştürüyor.

Cache Pre-warming ile Deno#

Deno’nun mimarisi modül çözümlemesini ve derlemeyi cache’liyor. Docker build sırasında uygulamayı önceden çalıştırmak bu cache’leri doldurur:

FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS adapter
FROM denoland/deno:bin-2.6.3 AS deno-bin
FROM debian:bookworm-slim

# Deno'yu yükle
COPY --from=deno-bin /deno /usr/local/bin/deno
COPY --from=adapter /lambda-adapter /opt/extensions/lambda-adapter

WORKDIR /var/task
ENV DENO_DIR=/var/deno_dir

# Uygulamayı kopyala
COPY . .

# Kritik: Deno cache'lerini ön-ısıt
# Bu, runtime cache'lerini doldurmak için build sırasında uygulamayı bir kez çalıştırır
RUN timeout 10s deno run --allow-net main.ts || [ $? -eq 124 ] || exit 1

ENV PORT=8080
CMD ["deno", "run", "--allow-net", "main.ts"]

timeout 10s komutu build sırasında uygulamayı çalıştırıyor, Deno’nun tüm modül çözümlemesini ve derlemeyi cache’lemesine izin veriyor. Exit code 124 (timeout) bekleniyor ve kabul edilebilir; burada amaç cache’leri doldurmak, sunucuyu ayakta tutmak değil.

Container Image’ları Build Etme ve Deploy Etme#

# Doğru mimari için build et (Apple Silicon'da kritik)
docker build \
  --platform linux/amd64 \
  --provenance=false \
  -t bun-lambda:latest .

# ECR'ye authenticate ol
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin ${ECR_URI}

# Tag'le ve push et
docker tag bun-lambda:latest ${ECR_URI}:latest
docker push ${ECR_URI}:latest

# Lambda function oluştur
aws lambda create-function \
  --function-name bun-container-function \
  --package-type Image \
  --code ImageUri=${ECR_URI}:latest \
  --role arn:aws:iam::123456789012:role/lambda-role

Platform spesifikasyonu kritik: Lambda varsayılan olarak x86_64 kullanıyor, ancak Apple Silicon’daki Docker varsayılan olarak arm64 kullanıyor. arm64 Lambda function’ları kullanmıyorsanız her zaman --platform linux/amd64 belirtin.

Performans Benchmark’ları#

Bu alanı ölçen iki yayımlanmış benchmark var ve runtime’ları farklı sıralıyorlar. İkisini yan yana okumak, her birinin tek başına anlattığından fazlasını veriyor.

128 MB’de Initialization ve Invocation#

Jason Butz, Mayıs 2025’te Lambda üzerinde bir JavaScript runtime karşılaştırması yayımladı ve CDK stack’ini GitHub’da bıraktı; böylece konfigürasyon anlatıya kalmadan incelenebiliyor: us-east-2, 128 MB, arm64, her runtime için EventBridge kuralıyla üç saatte bir tetiklenen SQS bağlantılı birer function, runtime başına 1.326 invocation ve yaklaşık 70 cold start, toplamda 3.978 invocation. Her invocation 50 adet SHA3-512 hash hesaplıyor. Node.js yönetilen nodejs22.x zip runtime’ında, Bun aarch64 layer’ıyla provided.al2023 üzerinde, Deno ise Lambda Web Adapter arkasında denoland/deno:bin-1.45.2 container image’ı olarak koşuyor.

Butz’un raporladığı initialization süreleri:

RuntimePaketlemeOrtalamap10p90
Node.js 22Yönetilen zip152,014ms145,555ms159,869ms
Deno 1.45.2Container image267,474ms184,607ms297,237ms
Bunprovided.al2023 üzerinde layer547,651ms500,075ms603,223ms

Bu ortalamalarda Deno container’ı yönetilen Node.js runtime’ından %76 daha yavaş başlıyor (267,474 / 152,014 = 1,76); Bun layer’ı ise %260 daha yavaş (547,651 / 152,014 = 3,60). Tersinden söylersek Node.js, Deno container’ının %43 ve Bun layer’ının %72 altında bir sürede ayağa kalkıyor.

Aynı koşudan invocation süreleri:

RuntimeOrtalamap50p90
Deno 1.45.213,708ms6,692ms19,836ms
Node.js 2221,290ms8,052ms56,711ms
Bun50,513ms15,190ms68,230ms

Warm invocation’da sıralama tersine dönüyor. Deno’nun ortalaması Node.js’in %36 altında ve p90’ı üçü arasında en dar olanı; üstelik container tabanlı bir custom runtime olarak çalışıyor.

Tablonun tamamını tek bir kısıt belirliyor: bu function’lar 128 MB ile koştu. AWS, Lambda’nın CPU gücünü yapılandırılan belleğe orantılı dağıttığını ve bir function’ın 1.769 MB’de bir vCPU eşdeğerine ulaştığını belgeliyor. Yani 128 MB’de dönen bir SHA3-512 döngüsü bir vCPU’nun yaklaşık on dörtte biriyle çalışıyor. Bu rakamlar, CPU tarafı ciddi biçimde kısıtlanmış tek bir konfigürasyonu anlatıyor.

Aynı Runtime’lar Container Olarak#

Deno, Temmuz 2024’te farklı bir kurulumla bir cold start çalışması yayımladı: 512 MB, us-west-2, x86_64 ve üç runtime’ın da Lambda Web Adapter arkasında container image olarak paketlenmesi. Her kombinasyon için 20 ila 25 zorlanmış cold start alınmış; sürümler Deno 1.45.2, Bun 1.1.19 ve Node.js 22.5.1. Bu, üreticinin kendi runtime’ını ölçtüğü bir benchmark; sıralamayı buna göre tartın. Ham veri ve harness da yayımlanmış durumda.

HTTP framework’üne göre ortalama initialization süreleri:

FrameworkDenoBunNode.js
Hono57,6ms98,6ms102,0ms
Express134,9ms178,8ms183,7ms
Fastify187,3ms273,0ms261,1ms

Node.js, Hono ve Express’te en yavaş, Fastify’da sondan ikinci. Bun ise 98,6ms ile 273,0ms arasında kalıyor; layer yolundaki 547,651ms’nin çok uzağında.

Çelişki Ne Gösteriyor#

Her çalışma tek başına bir runtime sıralaması gibi okunuyor. Yan yana konduklarında ikisinin de sabitlemediği değişkene, paketlemeye işaret ediyorlar.

Node.js ilk çalışmada öne geçiyor ve orada AWS’nin yönetilen zip runtime’ında koşan tek katılımcı da o. İkinci çalışmada herkes gibi container’a alındığında üç framework’ün ikisinde en yavaş olan Node.js oluyor. Bun’ın 547,651ms’si provided.al2023 üzerindeki layer yolundan geliyor; ikinci çalışma ise container’a alınmış Bun’ı Hono’da 100ms’nin altına indiriyor. İki çalışmanın hiçbiri runtime ile paketlemeyi birbirinden ayırmıyor. İlkinde her runtime farklı bir paketlemeyle koşuyor, yani ikisi birlikte değişiyor. İkincisi paketlemeyi runtime’lar arasında sabit tutuyor ama ilkinden bellek, bölge, mimari, framework ve workload bakımından ayrılıyor; dolayısıyla 547,651ms ile 98,6ms arasındaki mesafe de paketlemenin hanesine yazılamıyor. İkisinin birlikte desteklediği şey bir hipotez: initialization süresinde bir runtime’ın nasıl paketlendiği, hangi runtime olduğu kadar ağır basıyor olabilir. Bunu doğrulamak için tek bir runtime ve tek bir workload’ın aynı bellek, bölge ve mimaride üç şekilde de paketlenmesi gerekiyor: zip deployment paketi, provided.al2023 üzerinde layer ve container image. Yayımlanmış hiçbir benchmark bunu yapmıyor, dolayısıyla sıralama sınanmamış kalıyor.

Maliyet Analizi#

Lambda, request’i ve süreyi ayrı ayrı faturalandırıyor. AWS request başına $0,0000002 (milyon başına $0,20) listeliyor; bu ücret x86 ve arm64’te aynı. Süre ise en yakın milisaniyeye yukarı yuvarlanarak GB-saniye üzerinden ücretlendiriliyor. us-east-1’de ilk süre kademesi x86’da GB-saniye başına $0,0000166667, arm64’te $0,0000133334.

1 Ağustos 2025’ten itibaren Lambda INIT aşamasını her function konfigürasyonunda faturalandırıyor. AWS, custom runtime’ların, provisioned concurrency’nin ve OCI paketlemenin bu değişiklikten önce de INIT’i billed duration’a dahil ettiğini belirtiyor; yani yavaş bir custom runtime cold start’ı faturaya zaten hep yansıyordu.

İkinci girdi, cold start’ların ne sıklıkta yaşandığı. AWS bunların tipik olarak invocation’ların %1’inden azında görüldüğünü ve sürelerinin 100ms’nin altından bir saniyenin üzerine kadar değiştiğini belgeliyor. Bu sayı platformun tamamını anlatıyor, tek bir function için tavan değil. Butz’un kendi koşusunda runtime başına 1.326 invocation’da yaklaşık 70 cold start var, yani yaklaşık %5; çünkü üç saatlik tetikleme aralığı execution environment’ların iki tetikleme arasında boşta kalmasına izin veriyor. Seyrek çağrılan ve ani yükselen function’lar aynı sebeple %1’in üzerine çıkıyor. Oranı kendi function’ınızda ölçün, çünkü aşağıdaki modelin initialization kalemi bu oranla birlikte hareket ediyor.

Hesabın Adımları#

Milisaniyelerle ücretin aynı function’ı anlatması için Butz’un konfigürasyonunu alalım: arm64’te 128 MB, ilk kademe. Bu, $0,0000133334 × 0,125 GB ÷ 1000 = milisaniye başına $0,0000000016667 demek. Cold start oranı serbest değişken; tablo için %1’de sabitleyelim: ayda 10 milyon invocation’ın 100.000’i initialization ücreti ödüyor. Warm invocation satırları ise 10 milyonun tamamına uygulanıyor ve Butz’un SHA3-512 workload’ını da beraberinde taşıyor.

Node.js’e göre farkOlay başınaAyda 10 milyon invocation’da
Bun layer initialization, 395,637ms daha yavaş$0,00000066 ek$0,07 ek
Deno container initialization, 115,460ms daha yavaş$0,00000019 ek$0,02 ek
Bun warm invocation, 29,223ms daha yavaş$0,0000000487 ek$0,49 ek
Deno warm invocation, 7,582ms daha hızlı$0,0000000126 tasarruf$0,13 tasarruf

Aynı 10 milyon invocation’ın yalnızca request ücreti $2,00. İki initialization satırı da cold start oranıyla doğrusal ölçekleniyor. Butz’un koşusundaki yaklaşık %5’lik oranda Bun satırı $0,33; %10’da ise $0,66 olup kendi warm satırını geçiyor. Bu aralıkta runtime seçimi faturayı Bun tarafında yaklaşık bir dolar yukarı, Deno tarafında ise kuruşlar mertebesinde oynatıyor. Yönetilen runtime’da kalmanın gerekçesi cold start latency’si ve bakımını kendinizin üstlendiği bir runtime’ın operasyonel yükü.

Mimari Kaldıracı#

Mimari değiştirmek her function’a dokunuyor ve runtime değiştirmeye kıyasla geriye çok daha az bakım bırakıyor. AWS arm64 süresini GB-saniye başına $0,0000133334, x86’yı $0,0000166667 fiyatlıyor; bu, süre kaleminin tamamında %20 indirim demek. Yukarıdaki modelde bu indirimin karşılığı ayda yaklaşık $0,10. Node.js’in süre ücreti x86’da yaklaşık $0,48, arm64’te $0,38. Bu $0,10, Bun warm satırının eklediği $0,49’un altında ve Deno warm satırının kazandırdığı $0,13’e yakın. Süre kaleminden alınan bir yüzde, runtime satırlarını ancak o kalem büyüdüğünde geçiyor. 128 MB’de ve bu workload’da o kalem küçük kalıyor. arm64’ün ilk kademesi ayrıca %25 daha geniş (6 milyar yerine 7,5 milyar GB-saniye), yani en ucuz fiyatta daha uzun kalıyor. Request ücretleri ikisinde de aynı ve AWS, desteklenen tüm Lambda runtime’larının hem x86_64’ü hem arm64’ü desteklediğini belirtiyor.

Kararı aslında ne belirliyor:

  • Sabit trafik execution environment’ları sıcak tutuyor; Deno’nun kısa invocation süresi latency yüzdeliklerinde görünürken faturada kuruşlarda kalıyor
  • Ani yükselen trafik her scale-out’ta initialization farkını ödüyor ve bu sayı doğrudan kullanıcıya yansıyor
  • 128 MB’nin üzerindeki CPU-bound işler kendi ölçümünü gerektiriyor, çünkü CPU tahsisi yapılandırılan bellekle ölçekleniyor
  • Lambda workload’larının çoğunluğunu oluşturan I/O-bound function’lar zamanlarını ağı beklemekle geçiriyor; orada runtime iki sayıyı da pek oynatmıyor

Yaygın Tuzaklar#

Platform Mimari Uyumsuzluğu#

Yanlış CPU mimarisi için container image’ları build etmek şifreli runtime hataları oluşturur.

Semptom:

Error: Runtime exited with error: exit status 1
Runtime.InvalidEntrypoint

Temel sebep: Lambda varsayılan olarak x86_64 kullanıyor, ancak Apple Silicon’daki Docker varsayılan olarak arm64 kullanıyor.

Çözüm:

# Build'de her zaman platform belirt
docker build --platform linux/amd64 -t myfunction .

# Build edilmiş image'ı doğrula
docker inspect myimage:latest | grep Architecture
# Çıktı şu olmalı: "Architecture": "amd64"

Eksik Lambda Adapter Konfigürasyonu#

Container lokal olarak çalışıyor ancak Lambda’da bağlantı hatalarıyla başarısız oluyor.

Semptom: Function timeout oluyor veya 502 Bad Gateway döndürüyor.

Temel sebep: Adapter aksi söylenmedikçe trafiği 8080 portuna yönlendiriyor; 3000’e bağlanmış bir sunucu bu trafiği hiç görmüyor. Hedef portu AWS_LWA_PORT belirliyor ve bu değişken PORT’a geri düşüyor; çoğu örneğin yalnızca PORT set etmesinin sebebi de bu.

Doğru implementasyon:

# 8080 adapter'ın varsayılan hedefi, sunucunun da orada dinlemesi gerekiyor
ENV PORT=8080
CMD ["bun", "run", "server.ts"]

Farklı bir portta servis vermek isterseniz AWS_LWA_PORT değerini uygulamanın bağlandığı portla aynı yapın.

// Uygulamada environment variable kullan
const port = process.env.PORT || 3000;

Bun.serve({
  port: Number(port),
  fetch(request) {
    return new Response('Hello World');
  }
});

AWS SDK Uyumluluk Sorunları#

Daha eski Bun versiyonlarında Could not resolve: 'http2' hataları ve S3 ile SignatureDoesNotMatch hataları dahil AWS SDK uyumluluk zorlukları vardı. Son versiyonlar önemli ölçüde gelişti, ancak kendi kullanım senaryonuzda AWS SDK operasyonlarını her zaman açıkça test edin:

// test/aws-sdk.test.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { describe, test, expect } from 'bun:test';

describe('AWS SDK Uyumluluğu', () => {
  test('S3 PutObject çalışıyor', async () => {
    const client = new S3Client({ region: 'us-east-1' });
    const result = await client.send(new PutObjectCommand({
      Bucket: 'test-bucket',
      Key: 'test.txt',
      Body: 'test content'
    }));
    expect(result.$metadata.httpStatusCode).toBe(200);
  });
});

Dockerfile’da Bun versiyonunu sabitle:

# Kararlılık için specific version tag kullan
FROM oven/bun:1-debian

Lambda Layer Mimari Uyumsuzluğu#

Problem: Layer başarıyla deploy ediliyor ancak function “Runtime not supported” hatası veriyor.

Çözüm: Her iki mimari için de layer’lar build et ve publish et:

# x86_64 için build et
bun run publish-layer -- --arch x64
# Çıktı: arn:aws:lambda:us-east-1:123:layer:bun-x64:1

# arm64 için build et (bayrağın değeri aarch64)
bun run publish-layer -- --arch aarch64
# Çıktı: arn:aws:lambda:us-east-1:123:layer:bun-arm64:1

CDK’da layer ve function arasında mimari eşleştir:

import { Architecture } from 'aws-cdk-lib/aws-lambda';

const bunLayerX64 = LayerVersion.fromLayerVersionArn(
  this, 'BunLayerX64',
  'arn:aws:lambda:us-east-1:123:layer:bun-x64:1'
);

new Function(this, 'MyFunction', {
  architecture: Architecture.X86_64,
  layers: [bunLayerX64], // Eşleşmeli
});

Production-Ready İmplementasyon Pattern’leri#

Pattern 1: HTTP Sunucu ile Deno + Lambda Adapter#

API workload’larında iyi sonuç veren düzen şu:

// main.ts - oak framework ile Deno
import { Application } from "https://deno.land/x/oak@v17.1.6/mod.ts";

const app = new Application();

app.use((ctx) => {
  ctx.response.body = { message: "Hello from Deno on Lambda!" };
});

const port = parseInt(Deno.env.get("PORT") || "8080");
console.log(`Sunucu ${port} portunda çalışıyor`);
await app.listen({ port });
# Optimize edilmiş Dockerfile
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS adapter
FROM denoland/deno:bin-2.6.3 AS deno-bin
FROM debian:bookworm-slim

# Minimal dependency'ler
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

# Binary'leri kopyala
COPY --from=deno-bin /deno /usr/local/bin/deno
COPY --from=adapter /lambda-adapter /opt/extensions/lambda-adapter

WORKDIR /var/task
ENV DENO_DIR=/var/deno_dir PORT=8080

# Uygulama
COPY . .

# Cache'i ön-ısıt (kritik optimizasyon)
RUN timeout 10s deno run -A main.ts || [ $? -eq 124 ] || exit 1

CMD ["deno", "run", "-A", "main.ts"]

Deno container’ının initialization süresini yukarıda aktarılan aralığın alt ucunda tutan şey cache ön-ısıtması; Butz’un stack’indeki benchmark image’ı da aynı adımı uyguluyor. Lambda adapter handler’ı sıradan bir HTTP sunucusu olarak bıraktığı için aynı kod lokalde Lambda shim’i olmadan çalışıyor ve TypeScript build adımı gerektirmiyor.

Pattern 2: Hybrid Yaklaşım - Workload Başına Runtime#

Her function tipi için ona uyan runtime’ı kullan. Seçim dört duruma iniyor:

Function profiliRuntime
Cold start’a duyarlı ve AWS SDK ağırlıklıNode.js (managed)
Sabit warm trafikli, CPU-boundBir ölçüm Bun container’ını öne çıkarana kadar Node.js (managed)
TypeScript öncelikli, tahmin edilebilir trafikli background işDeno (container)
Geri kalan her şeyNode.js (managed)

O ikinci satır siz ölçene kadar açık kalıyor. Yukarıda ölçülen tek CPU-bound workload’da, 128 MB’de, Bun’ın warm invocation’ları üçü arasında en yavaş olanıydı. Bun’ın o satırı alması için kendi workload’ınızda ve bellek boyutunuzda yapılan bir ölçümün onu öne çıkarması gerekiyor.

Mimari örneği:

  • API Gateway endpoint’leri: Node.js (I/O-bound, cold start sensitive)
  • Image processing: Şimdilik Node.js, hedef bellek boyutunda Bun container’ı ölçülerek (CPU-intensive, yüksek bellek)
  • Scheduled task’lar: Deno container (TypeScript-native, tahmin edilebilir trafik)

Alternatif Yaklaşımlar#

Önce Node.js’i Optimize Et#

Runtime değiştirmeden önce, Node.js optimizasyonlarını değerlendir:

// Kötü: handler'da initialization
export async function handler(event: APIGatewayEvent) {
  const db = await createDatabaseConnection(); // Cold start cezası
  // ...
}

// İyi: modül seviyesinde initialization
const db = await createDatabaseConnection(); // Handler dışında

export async function handler(event: APIGatewayEvent) {
  // Önceden initialize edilmiş db'yi kullan
}

Tree shaking için ES Module’ler:

// Eski: CommonJS tüm modülü import eder
const AWS = require('aws-sdk');

// Yeni: ES Module'ler sadece gerekli kodu import eder
import { S3Client } from '@aws-sdk/client-s3';

Bu değişiklikler, runtime değiştirmenin karmaşıklığı olmadan çoğu zaman benzer kazancı veriyor.

Rust veya Go ile Maksimum Performans#

CPU-bound workload’larda derlenmiş diller her JavaScript runtime’ının önüne geçiyor ve Lambda bunları yukarıda anlatılan aynı provided.al2023 custom runtime arayüzü üzerinden çalıştırıyor.

Trade-off’lar:

  • Invocation başına daha hızlı execution ve daha düşük bellek kullanımı
  • Farklı bir dil, takım için beceri yatırımı demek
  • Daha uzun derleme süreleri, hızlı iterasyon için daha az esneklik

Runtime Seçimi#

API Gateway arkasındaki I/O-bound function’larda, ani yükselen trafikte ve AWS SDK’ya yaslanan kodda yönetilen Node.js runtime’ı varsayılan olmayı sürdürüyor. AWS initialization’ı kendi runtime’ları için ayarlıyor ve güvenlik güncellemelerini siz uğraşmadan uyguluyor. Container image’ları bu işi size geri veriyor: AWS, image’ı en güncel base image’dan yeniden build edip yeniden deploy etmenin sizin sorumluluğunuzda olduğunu ve bu şekilde paketlenen function’lar için deprecation bildirimi gönderilmediğini belirtiyor. Çevredeki tooling de Node.js varsayıyor.

Bu varsayılanı ancak şu üçü bir aradaysa değiştirin: function CPU-bound, trafik execution environment’ları sıcak tutacak kadar sabit ve elinizde ölçülmüş bir cold start bütçesi var. O durumda container image daha güçlü seçenek, çünkü cache’i önceden ısıtmaya izin veriyor. Bedeli base image yamaları, daha uzun deployment’lar ve ECR depolama maliyeti. Layer’lar daha hızlı deploy oluyor ve function’lar arasında paylaşılabiliyor. Buna karşılık mimari eşleştirmeyi zorunlu kılıyorlar ve AWS’nin layer’lar ile custom runtime’lar dahil deployment paketi için koyduğu 250 MB sıkıştırılmamış limite dahil oluyorlar. Bugün ikna etmesi daha zor olan runtime Bun; bunun bir kısmını paketleme açıklıyor olabilir. Yukarıdaki rakamlarda en yüksek initialization maliyetini layer yolu taşıyor; container’a alınmış Bun ise diğerleriyle aynı bantta duruyor. Yayımlanmış hiçbir benchmark paketlemeyi ayrıştırmadığı için bu sıralamayı kendi function’ınızda sınanacak bir hipotez olarak görün. Her iki durumda da yaslanabileceğiniz production troubleshooting kaynağı daha az.

Hangi yöne giderseniz gidin, önce Node.js tarafındaki seçenekleri tüketin: modül seviyesinde initialization, ES module import’ları ve provisioned concurrency. Hiçbiri operasyonel yük eklemiyor. Proof of concept yine de alternatif runtime’ı öne çıkarıyorsa, işi kritik olmayan tek bir function’da tutun; initialization duration ile cold start oranını Node.js baseline’ına karşı karşılaştırın. Bir cold start sayısına güvenmeden önce Lambda Runtime Interface Emulator ile lokal test edin.

Kaynaklar#

İlgili yazılar