Lonjakan latensi pasca-deploy pada dynamic Server-Side Rendering (SSR) sering kali melumpuhkan throughput aplikasi Node.js. Artikel ini mengulas investigasi insiden latensi P99 pada route SSR Next.js menggunakan OpenTelemetry (OTel), mulai dari pembacaan trace waterfall, konfigurasi instrumentation runtime, kriteria rollback otomatis, hingga penegakan timeout budget.

Postmortem Ringkas: Kronologi Lonjakan Latensi SSR

Sebuah deploy produksi pada rute dinamis /products/[id] memicu peningkatan drastis pada metrik Time to First Byte (TTFB). Sebelum rilis, P99 TTFB stabil pada 180ms. Lima menit pasca-deploy, P99 melonjak ke 4.800ms dan tingkat error 504 Gateway Timeout pada ingress controller meningkat hingga 6.2%.

Kronologi Insiden

  1. 14:02 UTC: Deployment v2.4.0 rampung via rolling update pada cluster Kubernetes.
  2. 14:07 UTC: Alert P99 TTFB breached (> 2.000ms selama 5 menit berturut-turut) menyala. Pod CPU utilization naik dari 22% ke 88%.
  3. 14:11 UTC: SRE mengamati queue connection pool Node.js jenuh. Event loop lag melonjak hingga 420ms.
  4. 14:14 UTC: Distributed tracing via OpenTelemetry menunjukkan akar masalah: penambahan fitur metadata inventori memicu sequential downstream HTTP fetch (waterfall call) sebanyak 4 panggilan per request SSR, menggantikan batch fetch sebelumnya.
  5. 14:18 UTC: Tim melakukan rollback ke versi v2.3.9. Latensi kembali normal dalam 90 detik.

Setup Observability: OpenTelemetry pada Container Next.js Standalone

Next.js menyediakan hook instrumentasi native pada runtime server. Untuk mendeteksi bottleneck I/O dan render compute, OpenTelemetry Node SDK harus diinisialisasi sebelum kode aplikasi dijalankan.

1. Konfigurasi next.config.js

Aktifkan instrumentationHook dan gunakan output mode standalone untuk deployment berbasis Docker.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  experimental: {
    instrumentationHook: true,
  },
};

module.exports = nextConfig;

2. Entry Point instrumentation.ts

File ini diletakkan di root project (atau di dalam direktori src/ jika menggunakan src-dir). Next.js mengeksekusi fungsi register() sekali saat server process boot.

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    await import('./instrumentation.node');
  }
}

3. Bootstrap OpenTelemetry SDK

Pisahkan inisialisasi SDK ke file runtime spesifik Node.js guna menghindari conflict modul pada Edge runtime.

// instrumentation.node.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: 'nextjs-storefront',
    [ATTR_SERVICE_VERSION]: process.env.APP_VERSION || '1.0.0',
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://otel-collector:4317',
  }),
  instrumentations: [
    new HttpInstrumentation(),
  ],
});

sdk.start();
Catatan container: Pastikan variabel lingkungan NODE_OPTIONS='--require ./instrumentation.node.js' tidak lagi diperlukan jika menggunakan fitur bawaan instrumentationHook. Hindari instrumentasi ganda yang melipatgandakan span overhead.

Analisis Trace Span: Membedakan Compute SSR vs I/O Bottleneck

Saat meneliti trace waterfall SSR, dua kategori latensi utama harus dipisahkan secara akurat:

  • Compute Latency (CPU bound): Kompilasi React Server Component (RSC), parsing payload JSON besar, serialization state, dan dynamic HTML rendering. Terlihat pada span Render Route /products/[id] atau Resolve React Component.
  • I/O Latency (Network/DB bound): Menunggu downstream network I/O, database query, atau upstream cache misses. Terlihat pada span bertipe HTTP GET atau prisma:client:operation.

Membaca Pola Waterfall pada Trace

Pada insiden v2.4.0, span trace menunjukkan struktur berurutan:

[Span: Next.js SSR Handler] -------------------------------------------------> 4.750ms
  ├── [Span: fetch GET /api/v1/product/123] ----------> 320ms
  ├── [Span: fetch GET /api/v1/inventory/123] (starts at 325ms) ------> 1.100ms
  ├── [Span: fetch GET /api/v1/reviews/123]   (starts at 1.430ms) -----> 1.450ms
  └── [Span: fetch GET /api/v1/pricing/123]   (starts at 2.885ms) -----> 1.800ms

Total waktu render habis bukan karena CPU rendering React (yang hanya menghabiskan 65ms pada trace), melainkan downstream waterfall I/O. Karena request dieksekusi secara serial di dalam komponen server asinkron, latensi terakumulasi langsung ke TTFB.

Strategi Mitigasi Cepat dan Kriteria Rollback

Jangan menganalisis bug di server produksi ketika error budget terancam habis. Terapkan automasi rollback berdasarkan metrik kuantitatif.

Indikator Trigger Rollback

  • Error Budget Burn Rate > 14.4x dalam 5 menit (mengindikasikan 2% error budget bulanan habis dalam 1 jam).
  • P99 Latency > 3x baseline SLO (misal baseline 300ms, breached jika > 900ms) selama 3 menit pada traffic normal.
  • Node.js Event Loop Lag > 200ms konsisten pada pod produksi.

Perintah Rollback Segera

# Rollback deployment Kubernetes ke revisi sebelumnya
kubectl rollout undo deployment/nextjs-storefront -n production

# Verifikasi status rollback
kubectl rollout status deployment/nextjs-storefront -n production

Pencegahan Permanen

1. Penegakan Timeout Budget via AbortSignal

Komponen SSR tidak boleh membiarkan I/O downstream menggantung tanpa batas. Terapkan timeout budget ketat di setiap remote call:

// lib/fetcher.ts
export async function fetchWithBudget(url: string, timeoutMs: number = 800): Promise<Response> {
  try {
    return await fetch(url, {
      signal: AbortSignal.timeout(timeoutMs),
      cache: 'no-store',
    });
  } catch (error: unknown) {
    if (error instanceof DOMException && error.name === 'TimeoutError') {
      throw new Error(`Downstream call to ${url} exceeded budget of ${timeoutMs}ms`);
    }
    throw error;
  }
}

2. Konfigurasi Trace Sampling untuk Skala Produksi

Mengirim 100% trace pada route dengan volume tinggi akan menambah overhead CPU dan biaya storage backend observability. Gunakan ParentBasedSampler dengan TraceIdRatioBasedSampler pada OpenTelemetry NodeSDK:

import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-node';

// Ganti konfigurasi sampler pada NodeSDK:
sampler: new ParentBasedSampler({
  root: new TraceIdRatioBasedSampler(0.05), // Sample 5% traffic root request
}),

3. Setup Alert Rule (Prometheus/VictoriaMetrics)

Deteksi degradasi TTFB SSR sebelum menyentuh batas kritis:

- alert: NextJsHighP99LatencySSR
  expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{route=~"/products/.*"}[5m])) by (le)) > 1.0
  for: 3m
  labels:
    severity: critical
  annotations:
    summary: "High P99 Latency on Next.js SSR route"
    description: "P99 latency on route {{ $labels.route }} is {{ $value }}s (threshold: 1.0s)."