Lonjakan trafik tiba-tiba yang melumpuhkan database tidak selalu berasal dari serangan DDoS eksternal. Dalam arsitektur aplikasi mobile, skenario umum yang sering memicu insiden fatal adalah thundering herd problem akibat reconnect storm. Artikel ini membedah kasus nyata database connection pool exhaustion yang dipicu oleh aplikasi React Native saat konektivitas jaringan pengguna pulih secara serentak.

1. Gejala Insiden: Pool Starvation dan HTTP 504

Insiden bermula ketika sistem monitoring mendeteksi lonjakan error secara mendadak pada API Gateway dan layanan backend utama. Gejala yang teramati pada sistem meliputi:

  • Active Connections Mencapai Batas Maksimum: Jumlah koneksi aktif pada database cluster (PostgreSQL) melonjak tajam menyentuh batas max_connections (misalnya 200/200 pool allocation habis terpakai dalam hitungan detik).
  • Pool Acquisition Timeout: Log aplikasi backend mencatat error fatal seperti TimeoutError: ResourceRequest timed out atau Knex/TypeORM error Timeout acquiring a connection. Worker process tidak bisa mengeksekusi query karena antrean memperebutkan koneksi pool kosong melebihi batas waktu tunggu default (biasanya 10–30 detik).
  • Cascade Failure HTTP 504 (Gateway Timeout): Load balancer dan API Gateway kehabisan downstream timeout karena permintaan HTTP tertahan di layer aplikasi yang sedang memblokir event loop/thread menunggu koneksi DB rilis.

2. Root Cause: Thundering Herd via NetInfo React Native

Penyebab utama insiden bukan berasal dari lambatnya query database, melainkan pola rekoneksi di sisi client. Pada arsitektur aplikasi mobile React Native, modul seperti @react-native-community/netinfo lazim digunakan untuk mendeteksi perubahan status koneksi data.

Masalah timbul ketika implementasi sinkronisasi data offline dieksekusi secara instan tanpa kontrol konkurensi:

// Anti-Pattern: Trigger flush instan tanpa penundaan atau jitter
NetInfo.addEventListener(state => {
  if (state.isConnected && state.isInternetReachable) {
    // Ribuan perangkat mengeksekusi ini secara serentak
    offlineQueue.flush(); 
  }
});

Ketika terjadi gangguan jaringan regional—seperti BTS seluler mengalami flicker, terowongan kereta, atau pemulihan jaringan setelah outage provider—puluhan ribu perangkat mobile beralih dari kondisi offline ke online secara bersamaan. Seluruh instance client langsung mengeksekusi antrean mutasi HTTP (POST/PUT data sinkronisasi) dalam sub-detik yang sama. Serangan rekoneksi massal ini (reconnect storm) menghantam backend secara instan tanpa proses perataan beban (smoothing).

3. Diagnosa Metrik dan Karakteristik Trafik

Langkah penelusuran akar masalah dilakukan melalui tiga observasi analitik utama:

  • Distribusi Arrival Rate: Metrik Request Per Second (RPS) tidak memperlihatkan kurva eksponensial bertahap, melainkan lonjakan vertikal curam (step increase) hingga 30–50 kali lipat dari baseline dalam rentang 500 ms.
  • Pool Metrics vs DB CPU: Metrik database CPU berada di level rendah (10–20%), tetapi metrik pool.waiting_clients menumpuk ribuan antrean. Ini mengindikasikan bottleneck berada pada overhead konkurensi pool dan I/O koneksi baru, bukan eksekusi kalkulasi query.
  • Endpoint Correlation: 90% trafik yang membanjiri server mengarah ke endpoint sinkronisasi mutasi (misal /api/v1/sync/batch) dengan signature header yang sama dari background task mobile.

4. Solusi Sisi Backend: Fail-Fast, Queue Limit, dan Load Shedding

Langkah pertama adalah melindungi database dari saturasi connection pool agar backend tidak crash total ketika dihantam lonjakan trafik berikutnya.

Konfigurasi Pool Timeout Ketat

Turunkan waktu acquireTimeoutMillis pada pool manager. Membiarkan request menunggu 30 detik pada antrean pool hanya menghabiskan memory worker dan socket gateway. Lebih baik gagal cepat (fail-fast):

// Contoh konfigurasi pool (pg-pool / generic-pool)
const poolConfig = {
  max: 50,
  idleTimeoutMillis: 10000,
  acquireTimeoutMillis: 2000, // Gagal cepat dalam 2 detik jika pool penuh
};

Load Shedding Middleware dengan HTTP 429

Terapkan middleware load shedding sebelum request menyentuh database query layer. Jika kapasitas antrean connection pool mendekati batas aman, tolak request seketika menggunakan status HTTP 429 Too Many Requests serta berikan header Retry-After:

import { Request, Response, NextFunction } from 'express';
import { dbPool } from './database';

export function poolSheddingMiddleware(req: Request, res: Response, next: NextFunction) {
  const MAX_QUEUE_LIMIT = 30;
  
  // Periksa antrean thread yang menunggu pool connection
  if (dbPool.waitingCount > MAX_QUEUE_LIMIT) {
    // Beri sinyal pada client untuk menunda request (misal antara 5-15 detik)
    const retrySeconds = Math.floor(Math.random() * 10) + 5;
    res.setHeader('Retry-After', retrySeconds.toString());
    
    return res.status(429).json({
      code: 'DB_SATURATED',
      message: 'Server sedang memproses antrean tinggi. Silakan coba kembali.',
    });
  }

  next();
}

5. Solusi Sisi React Native: Exponential Backoff dengan Full Jitter

Perbaikan di backend hanya berfungsi sebagai sistem pengaman (safety net). Sumber masalah utama di client harus diperbaiki dengan mendistribusikan waktu eksekusi antrean offline.

Mengapa Exponential Backoff Saja Tidak Cukup?

Exponential backoff standar tanpa randomisasi hanya menunda gelombang thundering herd ke interval berikutnya. Ribuan client yang gagal pada detik 0 akan serentak mencoba kembali pada detik 2, detik 4, dan detik 8 secara berkelompok. Solusi standar industri adalah menggunakan Full Jitter.

Formula Full Jitter:

sleep = random_between(0, min(max_delay, base_delay * 2 ** attempt))

Implementasi Offline Queue Sync

Berikut implementasi sinkronisasi aman pada React Native:

import NetInfo, { NetInfoState } from '@react-native-community/netinfo';

const BASE_DELAY_MS = 1000;
const MAX_DELAY_MS = 30000;
let isSyncing = false;

function calculateFullJitter(attempt: number): number {
  const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
  // Nilai random dari 0 hingga exponentialDelay meratakan arrival rate secara seragam
  return Math.floor(Math.random() * exponentialDelay);
}

async function processQueueWithBackoff(attempt = 0): Promise<void> {
  const jitterDelay = calculateFullJitter(attempt);
  await new Promise(resolve => setTimeout(resolve, jitterDelay));

  try {
    await sendOfflineMutations();
  } catch (error: any) {
    // Tangani respon load shedding dari backend
    if (error.response?.status === 429) {
      const retryAfterHeader = error.response.headers['retry-after'];
      const retryAfterSeconds = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 5;
      
      await new Promise(resolve => setTimeout(resolve, retryAfterSeconds * 1000));
      return processQueueWithBackoff(attempt + 1);
    }

    // Retry hingga batas percobaan wajar
    if (attempt < 5) {
      return processQueueWithBackoff(attempt + 1);
    }
  }
}

export function initializeNetworkListener() {
  NetInfo.addEventListener((state: NetInfoState) => {
    if (state.isConnected && state.isInternetReachable && !isSyncing) {
      isSyncing = true;
      processQueueWithBackoff(0).finally(() => {
        isSyncing = false;
      });
    }
  });
}

6. Evaluasi dan Monitoring

Kombinasi perbaikan backend dan client mengubah bentuk kurva traffic arrival rate dari paku vertikal (spike) tajam menjadi bukit landai yang terdistribusi merata sepanjang beberapa detik/menit.

Rekomendasi Produksi: Selalu atur alokasi koneksi connection pool database dengan formula: ((core_cpu * 2) + effective_spindle_count) per node database, jangan menaikkan max_connections secara serampangan untuk mengatasi pool exhaustion karena justru akan memperburuk context switching pada CPU database engine.