# High Availability Background Worker dengan Redis dan Cron

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/high-availability-background-worker-dengan-redis-dan-cron

---

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

Pukul 02:14 WIB, ponsel saya bergetar liar dengan nada alarm prioritas tertinggi dari PagerDuty. Layar menampilkan metrik yang membuat darah mendadak berdesir: *API Gateway Error Rate* melonjak menembus angka 42%, sementara *Queue Lag* untuk pemrosesan transaksi pembayaran dan pengiriman email transaksional melompat dari rata-rata 300 milidetik menjadi lebih dari 45 menit. Di ruang kendali virtual, rekan-rekan backend engineer mulai bersuara panik di kanal Slack darurat. Transaksi pelanggan tertahan, webhook dari payment gateway mulai berguguran karena timeout, dan antrean *background worker* kita yang biasanya tenang melaju kencang kini tampak lumpuh total. 

Sistem yang kitaandalkan—kombinasi klasik PHP-FPM di balik Nginx, dikombinasikan dengan Redis sebagai broker antrean dan cron bawaan sistem operasi untuk penjadwalan—mendadak kolaps di tengah malam buta. Ini bukan sekadar lonjakan trafik musiman, melainkan sebuah kegagalan sistemik yang menguji ketahanan infrastruktur kita sampai batas maksimal. Dan untuk memahaminya, kita harus menyelam jauh ke dalam ruang mesin tempat data dan perintah saling berkejaran tanpa henti.

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

Tanpa membuang waktu, saya langsung membuka terminal, melompat melalui SSH bastion host menuju *worker node* utama, dan mulai menarik log dari sistem manajemen proses supervisor serta memantau kesehatan Redis. Perintah pertama yang saya eksekusi adalah memantau status antrean secara langsung menggunakan CLI Redis:

```bash
# Memeriksa ukuran antrean dan memori Redis yang tersisa
redis-cli -h redis.internal.net -p 6379 info memory
redis-cli -h redis.internal.net -p 6379 llen queue:background_jobs
```

Angka yang muncul di layar sangat mencengangkan: `queue:background_jobs` mencatat lebih dari 2,4 juta *pending items*, sementara penggunaan memori Redis telah menyentuh batas maksimal alokasi (OOM limit) dengan kebijakan `noeviction` yang aktif. Akibatnya, setiap kali *worker* mencoba mendorong data baru atau memperbarui status tugas, Redis menolak perintah tersebut dengan error `OOM command not allowed when used memory > 'maxmemory'`. 

Namun, kejanggalan tidak berhenti di situ. Saat saya memeriksa log Supervisor (`/var/log/supervisord.log`), saya menemukan fenomena aneh: puluhan proses worker mendadak mati bersamaan (SIGTERM), lalu hidup kembali beberapa detik kemudian, memicu badai koneksi baru (*connection storm*) ke Redis yang langsung melumpuhkan port 6379 dalam sekejap. Penyelidikan awal menunjukkan adanya bentrok eksekusi cron job ganda yang mencoba menembakkan tugas masif secara bersamaan, memicu *memory spike* yang fatal pada satu node, yang kemudian merembet ke seluruh klaster. Dari sinilah kita mulai menarik benang merah menuju akar masalah yang sebenarnya.

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

Setelah menggali lebih dalam ke dalam kode sumber dan konfigurasi infrastruktur, kita menemukan tiga dosa besar arsitektural yang selama ini dibiarkan tidur tenang. Pertama, penggunaan *native system cron* (`/etc/cron.d/`) di beberapa *worker node* terpisah tanpa mekanisme *Leader Election*. Ketika jam menunjukkan pukul 02:00, setiap node secara serentak mengeksekusi skrip `php artisan schedule:run` (atau setara di stack Node.js/Go), memicu *duplicate job dispatch* sebanyak jumlah node yang aktif.

Kedua, implementasi antrean di Redis menggunakan struktur data List (`LPUSH` dan `BRPOPLPUSH`) tanpa batas TTL (*Time To Live*) dan tanpa manajemen *Dead Letter Queue* (DLQ). Ketika sebuah *payload* tugas gagal diproses karena kesalahan sintaks atau timeout database, *worker* secara naif memasukkannya kembali ke antrean utama tanpa henti, menciptakan *infinite retry loop* yang dengan cepat melahap seluruh alokasi RAM Redis. 

> [!WARNING]
> Jangan pernah mengandalkan *default system cron* untuk memicu tugas terdistribusi di lingkungan multi-server tanpa mekanisme penguncian (*locking*). Tanpa *distributed lock*, Anda sedang merakit bom waktu yang siap meledak saat skala infrastruktur Anda bertambah.

Ketiga, konfigurasi PHP-FPM dan pool koneksi Redis tidak memiliki *graceful shutdown timeout* yang memadai. Ketika proses worker dihentikan secara mendadak oleh skrip deployment atau kehabisan memori, koneksi TCP ke Redis terputus secara tidak normal, meninggalkan *orphan connections* yang menumpuk hingga memicu `maxclients` limit di sisi Redis. Transisi dari diagnosis ini menuntut kita untuk segera mengambil tindakan penyelamatan darurat sebelum layanan benar-benar mati total.

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

Situasi menuntut eksekusi cepat dan terukur. Kita tidak punya waktu untuk menulis ulang kode, melainkan harus memadamkan api yang sedang membakar infrastruktur. Berikut adalah rangkaian perintah darurat yang saya eksekusi langsung di terminal untuk memulihkan stabilitas Redis dan mengontrol kembali arus *worker*:

```bash
# 1. Hentikan sementara seluruh proses worker untuk mencegah badai koneksi
sudo supervisorctl stop worker-group-*

# 2. Flush koneksi klien yang menggantung di Redis untuk membebaskan port
redis-cli -h redis.internal.net client list | awk '{print $2}' | cut -d= -f2 | xargs -I {} redis-cli -h redis.internal.net client kill id {}

# 3. Pangkas sementara antrean yang macet parah atau pindahkan ke file backup darurat
redis-cli -h redis.internal.net lrange queue:background_jobs 0 10000 > /tmp/dead_jobs_backup.json
redis-cli -h redis.internal.net ltrim queue:background_jobs 10001 -1

# 4. Longgarkan kebijakan maxmemory Redis secara instan untuk memberikan ruang napas
redis-cli -h redis.internal.net config set maxmemory-policy allkeys-lru

# 5. Nyalakan kembali worker secara bertahap (staggered startup)
sudo supervisorctl start worker-group-01
sleep 10
sudo supervisorctl start worker-group-02
```

Langkah taktis di atas berhasil menurunkan penggunaan memori Redis dari 100% ke kisaran 45% dalam kurun waktu kurang dari tiga menit. Grafik error di Grafana mulai melandai, dan antrean transaksi perlahan-lahan terkikis habis. Namun, memadamkan api hanyalah separuh dari tugas seorang engineer; pekerjaan sesungguhnya adalah memastikan api yang sama tidak pernah berkobar lagi di masa depan.

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

Pengalaman pahit pukul 02:00 pagi itu memaksa kita melakukan perombakan total pada arsitektur penjadwalan dan antrean. Kita meninggalkan *system cron* mentah dan beralih ke pendekatan *Distributed Cron* berbasis Redis menggunakan mekanisme *atomic locking* (`SET NX PX`). Berikut adalah cuplikan implementasi pola *Leader Election* yang kini kita gunakan dalam bahasa Go untuk memastikan hanya satu instans cron yang mengeksekusi tugas dalam satu waktu:

```go
package main

import (
    "context"
    "fmt"
    "time"
    "github.com/go-redis/redis/v8"
)

func AcquireCronLock(ctx context.Context, client *redis.Client, lockKey string, ttl time.Duration) (bool, error) {
    val := fmt.Sprintf("node-%d", time.Now().UnixNano())
    success, err := client.SetNX(ctx, lockKey, val, ttl).Result()
    if err != nil {
        return false, err
    }
    return success, nil
}

// Penggunaan dalam penjadwal latar belakang
func RunScheduledTask(ctx context.Context, client *redis.Client) {
    lockKey := "cron:lock:daily_settlement"
    acquired, err := AcquireCronLock(ctx, client, lockKey, 30*time.Second)
    if err != nil || !acquired {
        fmt.Println("Cron sedang dieksekusi oleh node lain, melewati siklus ini.")
        return
    }
    
    // Eksekusi logika bisnis kritis di sini
    fmt.Println("Berhasil mengunci leader, menjalankan tugas latar belakang...")
}
```

Selain *Distributed Cron*, kita juga merevolusi struktur antrean Redis dengan menerapkan pola *Reliable Queue* menggunakan perintah `RPOPLPUSH` (atau `LMOVE` di Redis modern) yang memindahkan tugas ke antrean *processing* sementara sebelum benar-benar dihapus. Jika worker mengalami crash, tugas yang menggantung di antrean *processing* dapat diambil kembali secara otomatis (*orphan recovery*). Kita juga menyiapkan *Dead Letter Queue* (DLQ) khusus untuk menampung *payload* yang gagal diproses setelah 3 kali percobaan, lengkap dengan mekanisme *exponential backoff* dan notifikasi otomatis ke kanal Slack tim engineering.

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

Arsitektur yang tangguh tidak lengkap tanpa sistem pengawasan yang proaktif. Kita tidak boleh lagi menunggu pelanggan melapor atau alarm PagerDuty berbunyi karena server sudah terlanjur tumbang. Kita membangun metrik kustom yang dikumpulkan oleh Prometheus dan divisualisasikan secara real-time di dashboard Grafana, dengan aturan peringatan (*alerting rules*) yang sangat presisi.

Berikut adalah konfigurasi aturan Prometheus (`prometheus-alerts.yml`) yang memantau kesehatan antrean Redis dan potensi kebuntuan worker secara otomatis:

```yaml
groups:
  - name: background-worker-alerts
    rules:
      - alert: RedisQueueBacklogHigh
        expr: redis_list_length{key="queue:background_jobs"} > 50000
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Antrean background worker menumpuk sangat tinggi"
          description: "Jumlah item dalam antrean melebihi 50.000 selama lebih dari 5 menit. Segera periksa kesehatan worker node."

      - alert: WorkerProcessDown
        expr: supervisor_process_state{state="RUNNING"} == 0
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Proses worker berhenti secara tidak normal"
          description: "Proses supervisor untuk worker latar belakang pada node {{ $labels.instance }} telah mati."

      - alert: RedisMemoryNearCapacity
        expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Penggunaan memori Redis mendekati batas maksimal"
          description: "Redis menggunakan lebih dari 85% dari alokasi maxmemory yang diizinkan. Risiko OOM crash tinggi."
```

Dengan sistem peringatan berlapis ini, tim on-call kini menerima sinyal bahaya jauh sebelum memori Redis habis atau antrean meluber ke angka jutaan. Pengalaman malam itu mengajarkan kita bahwa infrastruktur skala besar menuntut disiplin arsitektural yang ketat: hilangkan asumsi, amankan setiap titik kegagalan tunggal (*single point of failure*), dan selalu uji ketahanan sistem Anda sebelum dunia nyata yang mengujinya dengan cara yang paling kejam.