Serverless patterns adalah cara menyusun banyak function kecil yang dipicu event supaya bersama-sama mengerjakan tugas besar: membagi kerja lewat antrean, bertahan saat event terkirim dua kali, dan mengatur cold start. Bayangin dapur katering di hari pesanan besar. 40 nota masuk sekaligus, dan kepala dapur membagikannya ke beberapa juru masak (fan-out). Tiap juru masak yang baru datang harus menata mejanya dulu (cold start): lamanya tergantung seberapa berat tas peralatannya (ukuran paket), jenis kompornya (runtime), dan persiapan yang dia lakukan sebelum memasak (init code). Printer nota kadang mencetak nota yang sama dua kali, jadi juru masak yang rapi mengecek buku cap dulu sebelum memasak (handler idempotent).
🧪 Tebakan cepat: supaya gak kena cold start, Lambda di-ping tiap 5 menit (keep-warm). Lalu 20 pesan datang bersamaan dan konkurensinya 20. Berapa environment yang tetap kena cold start? Tebak dulu, jawabannya ada di bagian "Provisioned concurrency vs keep-warm".
Cold start di jam virtual
Pelajaran AWS Lambda Deep Dive memperlihatkan Init Duration di baris REPORT. Isinya terdiri dari beberapa bagian, dan tiap bagian punya penyebabnya sendiri. Angka contoh dari Lab:
| Bagian cold start | Penyebab | Contoh di Lab |
|---|---|---|
| Memuat paket | ukuran kode dan dependency | paket 1 MB: 30 ms, paket 60 MB: 450 ms |
| Menyalakan runtime | bahasa dan mesinnya | Node.js 22: 120 ms, Python 3.13: 100 ms, Java 21: 900 ms |
| VPC | dulu membuat network interface saat cold start | 0 ms (sejak 2019 dibuat saat deploy) |
| Init code | kode di luar handler | buat client: 20 ms, ambil secret + buka koneksi DB: 380 ms |
Jadi Node.js dengan node_modules 60 MB dan init berat butuh 450 + 120 + 380 = 950 ms. Paket yang di-bundle jadi 1 MB dengan init ringan cuma 30 + 120 + 20 = 170 ms. Java dengan paket 60 MB dan init berat: 1.730 ms. VPC dulu terkenal menambah sampai sekitar 10 detik, tapi sejak 2019 AWS membuat network interface-nya saat function dibuat atau diubah, bukan saat cold start, jadi sekarang hampir gak berpengaruh.
Menekan cold start
- Kecilkan paket. Bundle dengan esbuild, dan import hanya client yang dipakai dari AWS SDK v3:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb", bukan paket lamaaws-sdk(v2). SDK v2 gak ada lagi di runtime Node.js 18 ke atas dan sudah berhenti didukung sejak September 2025. - Pilih runtime yang cepat menyala untuk endpoint yang sensitif latency. Java bisa dibantu SnapStart: Lambda menyimpan snapshot environment setelah init, lalu cold start berikutnya cukup memulihkan snapshot itu (tersedia juga untuk Python dan .NET).
- Buat client sekali, di luar handler.
const db = new DynamoDBClient({})di luar handler jalan sekali saat init, lalu dipakai ulang di tiap warm start. Kalau dipindah ke dalam handler, client dibuat ulang di tiap invocation dan gak pernah dipakai ulang. - Tunda yang jarang dipakai. Library besar yang cuma dipakai satu route di-
import()di dalam route itu, bukan di init.
Provisioned concurrency vs keep-warm
Provisioned concurrency menyiapkan sejumlah environment yang sudah di-init sebelum request datang. Dengan 5 environment 1 GB, biayanya sekitar $54 per bulan (harga contoh), dibayar walau gak ada request sama sekali. Di Lab, konkurensi 5 + provisioned 5 membuat ekspor jalan tanpa satu pun cold start.
Keep-warm adalah trik lama: EventBridge memanggil function tiap 5 menit supaya environment-nya gak dibuang. Murah, tapi satu ping cuma menjaga satu environment. Saat 20 pesan datang bersamaan, 1 environment sudah warm dan 19 lainnya tetap cold start. Itu jawaban tebakannya. Kalau latency harus stabil dan tim siap membayar, pakai provisioned concurrency.
Fan-out lewat antrean
Tombol "Ekspor semua" di catatan-api gak membuat 40 PDF di satu request. API memasukkan satu pesan per catatan ke antrean SQS ekspor-catatan, lalu Lambda catatan-ekspor-worker mengambilnya. Maximum concurrency di trigger SQS menentukan berapa environment yang bekerja bersamaan, sekaligus melindungi database dari banjir koneksi (pelajaran Connection Pooling). Hasil di Lab, 42 kiriman (lihat bagian berikutnya), 400 ms per PDF, cold start 950 ms:
| Maximum concurrency | Cold start | Selesai dalam |
|---|---|---|
| 2 | 2 | 9,35 detik |
| 5 | 5 | 4,55 detik |
| 20 | 20 | 2,15 detik |
| 5 + provisioned 5 | 0 | 3,6 detik |
Pola ini namanya fan-out: satu event dipecah jadi banyak kerja kecil yang jalan paralel. Pasangannya fan-in: menunggu semuanya selesai lalu menggabungkan hasilnya, misalnya menghitung item yang sudah selesai di tabel lalu membuat ZIP. Contoh klasiknya video yang baru di-upload: tiga Lambda jalan paralel untuk thumbnail, transcode ke HLS, dan metadata, lalu database di-update setelah ketiganya selesai. Pola lain: pipeline (tiap langkah menunggu langkah sebelumnya, misalnya S3 → validasi → SQS → proses) dan API composition (tiap route HTTP satu function).
Retry dan handler idempotent
SQS standard queue menjamin pesan terkirim at-least-once: paling sedikit sekali, kadang dua kali. Pesan yang gagal atau melewati visibility timeout juga dikirim ulang. Event async seperti S3 dicoba ulang Lambda sampai 2 kali kalau function-nya error. Di Lab, pesan #7 dan #23 datang dua kali (attributes.ApproximateReceiveCount = "2" di kiriman kedua), jadi ada 42 kiriman untuk 40 catatan.
Handler yang idempotent memberi hasil akhir yang sama walau event yang sama diproses dua kali, persis seperti Idempotency-Key di pelajaran Idempotency:
// catatan-ekspor-worker, dipicu SQS ekspor-catatan (batch size 1)
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
import { buatPdf, simpanPdf } from "./pdf.mjs";
// INIT: sekali per environment, dipakai ulang di warm start
const db = new DynamoDBClient({});
export const handler = async (event) => {
for (const record of event.Records) {
const { eksporId, catatanId } = JSON.parse(record.body);
const key = `ekspor/${eksporId}/${catatanId}.pdf`; // nama tetap: ditulis ulang, gak dobel
await simpanPdf(key, await buatPdf(catatanId));
try {
await db.send(new PutItemCommand({
TableName: "ekspor-item",
Item: { pk: { S: `${eksporId}#${catatanId}` }, file: { S: key } },
ConditionExpression: "attribute_not_exists(pk)",
}));
} catch (err) {
if (err.name !== "ConditionalCheckFailedException") throw err;
console.log(`duplikat dilewati: ${record.messageId}`);
}
}
};
- Key PDF dibuat dari
eksporIddancatatanId, jadi kiriman kedua menimpa file yang sama, bukan membuat file baru. ConditionExpression: "attribute_not_exists(pk)"membuat DynamoDB menolak item kedua denganConditionalCheckFailedException. Error itu ditangkap dan dilewati; error lain dilempar lagi supaya SQS mencoba ulang.- Versi yang gak idempotent memberi nama acak (
crypto.randomUUID()) ke file dan item. Di Lab hasilnya 42 PDF dan 42 baris untuk 40 catatan; versi di atas tetap 40. - DynamoDB dipakai di sini karena bicara lewat HTTP per request, jadi gak memegang koneksi seperti Postgres.
Kapan bukan serverless
- Proses panjang: Lambda berhenti di 15 menit. Encode video berjam-jam lebih cocok di container.
- Koneksi yang terus terbuka dan state di memori: game server multiplayer yang menjaga WebSocket, atau aplikasi stateful lainnya.
- Trafik tinggi yang stabil sepanjang hari: bayar per request bisa lebih mahal daripada server yang selalu menyala.
- Latency yang harus selalu rendah tapi gak mau membayar provisioned concurrency.
⚠️ Jebakan yang sering ketemu
- Membuat client di dalam handler. Client dan koneksinya dibuat ulang di tiap invocation. Buat sekali di luar handler.
- Mengandalkan keep-warm untuk lonjakan. Satu ping menjaga satu environment; lonjakan tetap cold start.
- Handler yang gak idempotent di belakang SQS. Pesan yang terkirim dua kali jadi PDF ganda, baris ganda, atau email ganda.
- Fan-out tanpa batas. Konkurensi 1.000 di depan Postgres menghabiskan koneksinya. Pasang maximum concurrency.
TL;DR: Cold start = memuat paket (ukuran) + menyalakan runtime (Node.js dan Python cepat, Java lambat tanpa SnapStart) + init code; VPC sejak 2019 hampir gak menambah apa-apa. Di Lab: 950 ms jadi 170 ms dengan paket 1 MB dan init ringan. Import modular dari AWS SDK v3 (v2 sudah berhenti didukung), dan buat client seperti new DynamoDBClient({}) sekali di luar handler; di dalam handler ia dibuat ulang di tiap invocation. Provisioned concurrency menjaga environment tetap warm dengan biaya tetap; keep-warm cuma menjaga satu, jadi lonjakan 20 pesan masih 19 cold start. Fan-out lewat SQS dengan maximum concurrency membagi kerja secara paralel tanpa membanjiri database, dan fan-in menggabungkan hasilnya (contoh: thumbnail, HLS, metadata paralel). SQS dan event async bisa mengirim event yang sama dua kali, jadi handler harus idempotent: nama hasil yang tetap dan conditional write. Serverless gak cocok untuk proses lebih dari 15 menit, WebSocket dan game server yang stateful, dan trafik tinggi yang stabil.