# Serverless WebSockets: Mengoptimalkan AWS API Gateway dan Lambda untuk Biaya Minimal

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/serverless-websockets-mengoptimalkan-aws-api-gateway-dan-lambda-untuk-biaya-minimal

---

# Post-Mortem & Incident Runbook: Serverless WebSockets di AWS

## 1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang

Pukul 02:14 WIB, pager di meja samping tempat tidurku menjerit melengking, membangunkan tidur nyenyak. Layar ponsel menampilkan notifikasi merah dari PagerDuty: metrik latensi WebSocket melonjak drastis dari rata-rata 45 milidetik menjadi lebih dari 8.000 milidetik, dibarengi dengan lonjakan error `504 Gateway Timeout` di dashboard CloudWatch. Sebagai Senior Staff Engineer yang bertanggung jawab atas infrastruktur real-time, malam ini berubah menjadi investigasi forensik kilat. Pengguna kami, yang mayoritas adalah trader finansial dan pengguna aplikasi kolaborasi real-time, mulai mengeluh bahwa pesan chat dan pembaruan grafik harga terputus total. Koneksi klien yang biasanya stabil tiba-tiba berguguran seperti daun di musim gugur, dan tagihan AWS billing harian melonjak tajam tanpa kendali yang jelas.

Sambil membuka laptop di dalam kamar yang remang-remang, aku langsung masuk ke terminal dan menyalakan VPN korporat. Ada dua hal yang langsung terpikir di kepala: apakah lambatnya layanan ini disebabkan oleh lonjakan trafik organik yang tak terduga, atau ada kebocoran sumber daya di lapisan backend yang membuat AWS API Gateway WebSocket kewalahan? Untuk menjawabnya, kita tidak bisa menebak-nebak; kita harus turun langsung ke dalam mesin, menelusuri log, dan membedah metrik satu per satu.

## 2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik

Tanpa membuang waktu, aku langsung melompat ke AWS CloudWatch Insights untuk menarik log dari AWS API Gateway dan AWS Lambda yang bertindak sebagai otak di balik komunikasi WebSocket ini. Kueri log awal langsung memperlihatkan pola yang sangat aneh namun familier bagi arsitek serverless yang kurang hati-hati:

```sql
fields @timestamp, @message
| filter @message like /Task timed out/
| stats count(*) as timeout_count by @logStream
| sort timeout_count desc
| limit 20
```

Hasilnya mengerikan. Ribuan eksekusi AWS Lambda mengalami timeout setelah berjalan selama 30 detik penuh. Aku beralih ke konsol DynamoDB tempat kami menyimpan pemetaan `connectionId` klien. Grafiknya menunjukkan kapasitas *Read/Write Capacity Units (RCU/WCU)* tercekik habis karena lonjakan *throttling*. 

Ternyata, setiap kali ada pesan masuk yang harus disiarkan (*broadcast*) ke ribuan klien aktif, fungsi Lambda kami melakukan pemindaian tabel (*Table Scan*) secara brutal ke DynamoDB untuk mencari siapa saja yang sedang online, alih-alih melakukan *BatchGet* atau query yang terindeks rapi. Lebih parah lagi, arsitektur yang kami bangun gagal mengantisipasi masalah *Cold Start* pada fungsi Lambda berukuran besar, ditambah tidak adanya penanganan ketika klien memutus koneksi secara paksa, meninggalkan tumpukan *stale connections* di dalam basis data.

## 3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya

Setelah menggali lebih dalam, akar masalah dari insiden dini hari ini mengerucut pada tiga dosa besar arsitektur serverless WebSocket:

1. **Desain Rute API Gateway yang Naif**: Kami mengandalkan `$default` route tanpa memetakan aksi spesifik secara efisien, memaksa Lambda memproses logika routing yang berat di dalam kode.
2. **Manajemen DynamoDB yang Buruk**: Menyimpan `connectionId` tanpa strategi partisi yang tepat dan melakukan operasi `Scan` alih-alih `Query` berbasis indeks sekunder global (GSI).
3. **Abaikan Cold Start & Keep-Alive**: Fungsi Lambda ditulis dalam Node.js dengan ukuran deployment monolitik yang membengkak hingga 80MB karena modul pihak ketiga yang tidak perlu, memicu *Cold Start* hingga 3,2 detik setiap kali terjadi lonjakan trafik mendadak.

> [!WARNING]
> Menggunakan operasi `Scan` pada DynamoDB untuk melacak koneksi WebSocket aktif adalah tiket express menuju tagihan AWS bengkak dan pelambatan sistem secara total.

Untuk memperbaiki ini secara permanen, kita harus merombak fondasi kode dan infrastrukturnya. Mari kita lihat bagaimana cara membangun rute, penyimpanan, dan fungsi Lambda yang kebal terhadap lonjakan.

## 4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit

Sambil merencanakan perbaikan jangka panjang, sistem produksi harus diselamatkan sekarang juga. Berikut adalah emergency runbook one-liner berbasis AWS CLI yang aku eksekusi di terminal untuk memangkas *concurrency* Lambda yang macet dan membersihkan koneksi mati secara paksa:

```bash
# Perintah darurat untuk membersihkan koneksi mati dan mereset throttle API Gateway
aws apigatewayv2 get-connections --api-id ${API_ID} --query "items[?connectedAt < `$(date -d '2 hours ago' -u +%Y-%m-%dT%H:%M:%SZ)`].connectionId" --output text | xargs -I {} aws apigatewayv2 delete-connection --api-id ${API_ID} --connection-id {}
```

Perintah tersebut langsung menyapu bersih ribuan koneksi zombie yang menggantung dan membebani memori Lambda. Sembari trafik mulai berangsur normal, mari kita tulis ulang arsitekturnya agar kejadian ini tidak terulang kembali.

Pertama, kita definisikan rute AWS API Gateway WebSocket yang bersih menggunakan Terraform:

```hcl
resource "aws_apigatewayv2_api" "websocket_api" {
  name                       = "production-websocket"
  protocol_type              = "WEBSOCKET"
  route_selection_expression = "$request.body.action"
}

resource "aws_apigatewayv2_route" "connect_route" {
  api_id    = aws_apigatewayv2_api.websocket_api.id
  route_key = "$connect"
  target    = "integrations/${aws_apigatewayv2_integration.lambda_connect.id}"
}

resource "aws_apigatewayv2_route" "disconnect_route" {
  api_id    = aws_apigatewayv2_api.websocket_api.id
  route_key = "$disconnect"
  target    = "integrations/${aws_apigatewayv2_integration.lambda_disconnect.id}"
}

resource "aws_apigatewayv2_route" "sendmessage_route" {
  api_id    = aws_apigatewayv2_api.websocket_api.id
  route_key = "sendMessage"
  target    = "integrations/${aws_apigatewayv2_integration.lambda_sendmessage.id}"
}
```

Selanjutnya, untuk menangani penyimpanan `connectionId` di DynamoDB tanpa membuat tagihan meledak, kita gunakan skema tabel yang dioptimalkan dengan TTL (Time-To-Live) agar koneksi yang terbengkalai otomatis terhapus:

```javascript
// Handler untuk $connect - Menyimpan Connection ID ke DynamoDB
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb");

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

exports.handler = async (event) => {
    const connectionId = event.requestContext.connectionId;
    const ttl = Math.floor(Date.now() / 1000) + 7200; // Expired dalam 2 jam jika tidak ada aktivitas

    const command = new PutCommand({
        TableName: process.env.CONNECTIONS_TABLE,
        Item: {
            connectionId: connectionId,
            ttl: ttl,
            connectedAt: new Date().toISOString()
        },
    });

    try {
        await docClient.send(command);
        return { statusCode: 200, body: 'Connected.' };
    } catch (err) {
        console.error("Gagal menyimpan koneksi:", err);
        return { statusCode: 500, body: 'Failed to connect: ' + JSON.stringify(err) };
    }
};
```

Terakhir, untuk mengatasi *Cold Start* yang mematikan, kita optimalkan fungsi Lambda dengan menggunakan *AWS Lambda Power Tuning*, memperkecil *bundle size* menggunakan esbuild, dan menerapkan *Provisioned Concurrency* hanya pada jam-jam puncak trafik.

> [!TIP]
> Gunakan `esbuild` untuk melakukan *bundling* kode Lambda Node.js Anda. Ukuran file yang lebih kecil di bawah 5MB terbukti memangkas waktu *Cold Start* hingga 70%.

## 5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang

Insiden pukul 2 pagi ini memberikan pelajaran mahal tentang bagaimana memperlakukan arsitektur serverless secara disiplin. Kami mengadakan sesi *blameless post-mortem* keesokan harinya dan menetapkan aturan baru yang wajib dipatuhi seluruh tim engineering:

- **Dilarang Keras Melakukan Scan pada Database Real-time**: Semua operasi pencarian koneksi wajib menggunakan partisi kunci yang spesifik atau memanfaatkan *Global Secondary Index* (GSI) dengan kapasitas *On-Demand* atau *Auto-Scaling* yang terkonfigurasi ketat.
- **Penerapan TTL Wajib**: Setiap entri koneksi di DynamoDB wajib memiliki atribut TTL untuk mencegah penumpukan data basi akibat klien yang terputus tanpa memicu event `$disconnect` secara sempurna.
- **Optimasi Ukuran Paket Lambda**: Setiap fungsi Lambda tidak boleh melebihi ukuran 10MB setelah di-kompres, dan wajib melaluiuji beban (*load testing*) menggunakan k6 sebelum disetujui masuk ke lingkungan produksi.

## 6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call

Agar kita bisa tidur dengan tenang tanpa takut dikejutkan oleh lonjakan latensi mendadak, kita pasang sistem peringatan dini menggunakan CloudWatch Alarms yang terhubung langsung ke webhook Slack dan PagerDuty tim on-call:

```hcl
resource "aws_cloudwatch_metric_alarm" "websocket_latency_alarm" {
  alarm_name          = "prod-websocket-high-latency"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "IntegrationLatency"
  namespace           = "AWS/ApiGateway"
  period              = 60
  statistic           = "Average"
  threshold           = 1000
  alarm_description   = "Alarm jika latensi integrasi WebSocket API Gateway melebihi 1 detik"
  alarm_actions       = [aws_sns_topic.pagerduty_alerts.arn]

  dimensions = {
    ApiId = aws_apigatewayv2_api.websocket_api.id
  }
}

resource "aws_cloudwatch_metric_alarm" "lambda_errors_alarm" {
  alarm_name          = "prod-websocket-lambda-errors"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = 60
  statistic           = "Sum"
  threshold           = 5
  alarm_description   = "Alarm jika terjadi error pada fungsi Lambda WebSocket"
  alarm_actions       = [aws_sns_topic.pagerduty_alerts.arn]

  dimensions = {
    FunctionName = aws_lambda_function.websocket_handler.function_name
  }
}
```

Dengan fondasi arsitektur yang tahan banting, penanganan DynamoDB yang optimal, serta sistem alarm otomatis yang siaga 24/7, ekosistem Serverless WebSocket kami kini siap menangani jutaan koneksi bersamaan dengan biaya operasional yang sangat efisien dan senyum lebar di wajah tim keuangan.