Gejala Insiden: Egress Melonjak dan Browser Klien Membeku

Sebuah endpoint daftar transaksi (GET /api/v1/orders) mengalami degradasi performa kritis di lingkungan produksi. Rata-rata transfer size melonjak dari 65 KB menjadi 34 MB per request untuk pagination standar 50 record. Lonjakan ini memicu tiga gejala teknis simultan:

  • Egress Network Spike: Bandwidth egress server melonjak ribuan persen dalam hitungan jam, menekan throughput jaringan gateway.
  • TTFB (Time to First Byte) Membengkak: Waktu respons backend naik dari 120 ms ke 4.800 ms. Sebagian besar waktu habis di CPU runtime aplikasi, bukan I/O query database.
  • Client Thread Locking: Aplikasi frontend web dan mobile mengalami freezing (UI thread drop ke 0 FPS). Main thread JavaScript terblokir selama proses JSON.parse() terhadap payload berukuran 34 MB.

Investigasi: Telemetri Jaringan dan Profiling Serializer

Pemeriksaan log access Nginx mengonfirmasi header Content-Length rata-rata mencapai 35.651.584 bytes. Hasil profiling APM (Application Performance Monitoring) menunjukkan waktu eksekusi query SQL hanya memakan waktu 45 ms, namun siklus request tertahan selama 4.200 ms sebelum response stream dikirim.

Isolasi profiler CPU menunjukkan bahwa 88% siklus runtime dialokasikan ke fungsi internal serializer JSON (seperti JSON.stringify pada Node.js atau json_encode pada PHP). Inspeksi data mentah payload mengungkap bahwa satu object Order memuat relasi bersarang tanpa batas:

Order
 └── Customer
      └── Orders (History)
           └── PaymentMethod
                └── AuditLogs (Snapshot metadata internal puluhan ribu baris)

Akar Masalah: Eager Loading Rekursif Tanpa Proyeksi

Insiden ini berakar dari anti-pattern pengembalian langsung entitas ORM ke response handler tanpa lapisan isolasi kontrak. Pengembang menambahkan eager loading untuk menghindari problem N+1 query:

// Anti-pattern: Memuat relasi berlapis dan mengembalikan raw entity
export async function getOrders(req: Request, res: Response) {
  const orders = await ormRepository.find({
    relations: [
      'customer',
      'customer.orders',
      'customer.orders.paymentMethod',
      'customer.orders.paymentMethod.auditLogs'
    ],
    take: 50
  });

  // Serialisasi langsung seluruh graph object ORM
  return res.json(orders);
}

ORM memuat instance model lengkap. Karena relasi bersifat dua arah (bidirectional) dan serializer default memproses semua public properties tanpa filter, data metadata audit internal yang seharusnya tidak terekspos ikut terangkut ke dalam payload.

Langkah Remediasi Teknis

1. Pemangkasan Kolom dan Query Projection

Hentikan pengambilan seluruh kolom database ke memori. Batasi kolom pada level query database menggunakan proyeksi eksplisit:

// Hanya ambil kolom yang dibutuhkan antarmuka pengguna
const orders = await ormRepository.find({
  select: {
    id: true,
    totalAmount: true,
    status: true,
    createdAt: true,
    customer: {
      id: true,
      name: true,
      email: true
    }
  },
  relations: ['customer'],
  take: 50
});

2. Penerapan DTO (Data Transfer Object) Eksplisit

Pisahkan internal database model dengan external API contract. Jangan pernah mengekspos entitas ORM langsung ke layer presentasi. Gunakan fungsi mapping deterministik:

interface OrderListItemDTO {
  id: string;
  total_amount: number;
  status: string;
  created_at: string;
  customer: {
    id: string;
    name: string;
  };
}

function toOrderListItemDTO(order: OrderEntity): OrderListItemDTO {
  return {
    id: order.id,
    total_amount: order.totalAmount,
    status: order.status,
    created_at: order.createdAt.toISOString(),
    customer: {
      id: order.customer.id,
      name: order.customer.name
    }
  };
}

// Mapping eksplisit sebelum serialisasi
return res.json(orders.map(toOrderListItemDTO));
Catatan Desain: DTO memastikan penambahan relasi atau kolom sensitif baru pada skema database tidak akan membocorkan data atau merusak ukuran payload API yang sudah berjalan di production.

3. Automated Regression Guard untuk Ukuran Payload

Guna mencegah regresi di masa depan, terapkan integration test di CI/CD pipeline yang memeriksa batas maksimum ukuran respons per endpoint kritis:

import request from 'supertest';
import { app } from '../app';

describe('GET /api/v1/orders payload budget', () => {
  const MAX_PAYLOAD_BYTES = 100 * 1024; // Budget: 100 KB untuk 50 records

  it('should not exceed maximum payload budget', async () => {
    const response = await request(app)
      .get('/api/v1/orders?limit=50')
      .expect(200);

    const payloadSize = Buffer.byteLength(JSON.stringify(response.body));

    expect(payloadSize).toBeLessThan(MAX_PAYLOAD_BYTES);
  });
});

Hasil dan Evaluasi

Setelah implementasi DTO dan pemangkasan relasi:

  • Ukuran payload berkurang dari 34 MB menjadi 42 KB per 50 records (reduksi 99,8%).
  • TTFB turun dari 4.800 ms menjadi 68 ms karena serialisasi JSON kini instan.
  • Beban parsing CPU pada sisi klien kembali normal di bawah 5 ms tanpa UI freeze.