Jitter #

Ada sebuah skenario yang sangat umum di distributed system: semua instance mengalami kegagalan pada waktu yang hampir bersamaan, semua menunggu dengan delay yang sama, lalu semua retry bersamaan. Downstream yang baru saja mulai pulih langsung dihantam gelombang request dan jatuh lagi. Siklus ini berulang — bukan karena ada bug baru, melainkan karena semua komponen bergerak dengan ritme yang identik. Inilah thundering herd problem, dan jitter adalah solusinya. Jitter bukan sekadar “tambahan random” — ia adalah mekanisme untuk memecah sinkronisasi yang tidak disengaja antara banyak instance yang bekerja secara bersamaan. Panduan ini membahas jitter dari masalah yang ia selesaikan, empat jenis implementasi dengan kode Go konkret dan perbandingan distribusinya, tujuh use case production dari retry hingga leader election, hingga panduan praktis kapan jitter wajib digunakan.

Masalah Nyata Tanpa Jitter #

Untuk memahami mengapa jitter diperlukan, perlu dipahami dulu apa yang terjadi ketika banyak instance bergerak dengan ritme yang identik.

Thundering Herd Problem #

Skenario: 500 consumer Kafka gagal proses message pada saat yang sama

t=0     Message queue overload → semua consumer gagal
t=5s    Semua consumer retry bersamaan → delay tetap 5 detik
        → Queue menerima 500 request sekaligus
        → Queue baru saja mulai pulih, langsung jatuh lagi
t=10s   Semua consumer retry lagi bersamaan
        → Siklus ini berulang tanpa akhir
        → Queue tidak pernah dapat waktu untuk benar-benar pulih

Skenario yang sama dengan jitter:

t=0     Semua consumer gagal
t=2–8s  Consumer retry tersebar dalam rentang 6 detik
        → Queue menerima 83 request per detik (500/6) bukan 500 sekaligus
        → Queue bisa proses secara bertahap
t=10s   Sebagian besar consumer berhasil
        → Sistem pulih secara gradual

Perbedaannya bukan pada total beban — jumlah request sama. Perbedaannya adalah distribusi dalam waktu. Backoff tanpa jitter hanya menunda thundering herd, tidak menghilangkannya.

Cache Stampede #

Thundering herd juga terjadi dalam bentuk lain: cache stampede. Ketika banyak cache key expired secara bersamaan, semua request yang biasanya dilayani cache tiba-tiba harus ke database secara bersamaan.

Tanpa jitter pada cache TTL:
  Set 10.000 cache entries pada t=0 dengan TTL=3600s
  Pada t=3600, semua expired bersamaan
  → 10.000 cache miss serentak → database overload

Dengan jitter pada TTL:
  TTL = 3600s + random(-300, +300)  ← ±5 menit jitter
  Entries expired tersebar dalam rentang 10 menit
  → ~17 cache miss per detik (bukan 10.000 sekaligus)

Empat Jenis Jitter #

Masing-masing jenis memiliki karakteristik distribusi yang berbeda dan cocok untuk konteks yang berbeda.

1. Full Jitter #

Delay dipilih secara sepenuhnya acak antara 0 dan nilai maksimum. Ini memberikan distribusi yang paling merata dan paling efektif menghilangkan sinkronisasi.

// Full Jitter — distribusi uniform antara 0 dan cap
func fullJitter(cap time.Duration) time.Duration {
    return time.Duration(rand.Int63n(int64(cap) + 1))
}

// Penggunaan dalam exponential backoff + full jitter
func backoffWithFullJitter(attempt int, base, max time.Duration) time.Duration {
    // Hitung exponential cap
    cap := base * time.Duration(1<<uint(attempt))
    if cap > max {
        cap = max
    }
    // Full jitter: random antara 0 dan cap
    return fullJitter(cap)
}

// Contoh distribusi untuk attempt=3, base=100ms, max=30s:
// Cap = 100ms × 2³ = 800ms
// Delay bisa: 12ms, 754ms, 387ms, 201ms, 634ms
// → Sepenuhnya tersebar, tidak ada pola

Full jitter adalah pilihan default yang direkomendasikan AWS untuk retry di distributed system. Kelemahannya: delay bisa sangat kecil (bahkan 0ms), yang berarti beberapa retry mungkin sangat cepat.

2. Equal Jitter #

Setengah delay adalah nilai tetap, setengahnya lagi random. Ini memberikan “floor” minimum sehingga tidak ada retry yang terlalu cepat.

// Equal Jitter — setengah deterministik, setengah random
func equalJitter(cap time.Duration) time.Duration {
    half := cap / 2
    return half + time.Duration(rand.Int63n(int64(half)+1))
}

func backoffWithEqualJitter(attempt int, base, max time.Duration) time.Duration {
    cap := base * time.Duration(1<<uint(attempt))
    if cap > max {
        cap = max
    }
    return equalJitter(cap)
}

// Contoh distribusi untuk cap=800ms:
// Selalu minimal 400ms, maksimal 800ms
// Delay bisa: 412ms, 778ms, 543ms, 621ms, 489ms
// → Tersebar tapi dengan minimum yang predictable

Equal jitter cocok untuk kasus di mana delay minimum penting — misalnya untuk memberi downstream waktu minimum yang cukup untuk mulai pulih.

3. Decorrelated Jitter #

Delay berikutnya bergantung pada delay sebelumnya dan tumbuh secara organik. Direkomendasikan oleh AWS untuk kasus di mana ingin ada korelasi natural antar retry.

// Decorrelated Jitter — AWS-style
// delay = random(base, min(max, prev_delay × 3))
func decorrelatedJitter(prev, base, max time.Duration) time.Duration {
    minDelay := base
    maxJitter := prev * 3
    if maxJitter > max {
        maxJitter = max
    }
    if maxJitter < minDelay {
        maxJitter = minDelay
    }
    return minDelay + time.Duration(rand.Int63n(int64(maxJitter-minDelay)+1))
}

// Contoh urutan dengan base=100ms, max=30s:
// prev=100ms → random(100ms, 300ms) → misal 187ms
// prev=187ms → random(100ms, 561ms) → misal 342ms
// prev=342ms → random(100ms, 1026ms) → misal 891ms
// prev=891ms → random(100ms, 2673ms) → misal 1240ms
// Tumbuh secara organik, tidak mengikuti pola yang predictable

4. Fixed Interval + Small Jitter #

Interval tetap dengan variasi kecil. Ini bukan untuk retry — melainkan untuk operasi berkala seperti heartbeat dan polling yang perlu berjalan pada interval yang kira-kira tetap tapi tidak persis sinkron antar instance.

// Fixed interval + small jitter untuk heartbeat
type HeartbeatConfig struct {
    BaseInterval time.Duration
    JitterRange  time.Duration // ±range
}

func (c *HeartbeatConfig) NextInterval() time.Duration {
    // Jitter ±JitterRange dari base interval
    jitter := time.Duration(rand.Int63n(int64(c.JitterRange)*2+1)) - c.JitterRange
    interval := c.BaseInterval + jitter
    if interval < 0 {
        return c.BaseInterval // floor: jangan sampai negatif
    }
    return interval
}

// Config: base=10s, jitter=±1s → interval antara 9–11 detik
heartbeat := &HeartbeatConfig{
    BaseInterval: 10 * time.Second,
    JitterRange:  1 * time.Second,
}

// Loop heartbeat
func (s *Service) runHeartbeat(ctx context.Context) {
    for {
        s.sendHeartbeat()
        interval := heartbeat.NextInterval()
        select {
        case <-time.After(interval):
        case <-ctx.Done():
            return
        }
    }
}

Perbandingan Distribusi — Visualisasi #

Tanpa jitter (fixed 5s): semua client retry pada t=5, 10, 15...
  t=5:  ████████████████████ (semua sekaligus)
  t=10: ████████████████████ (semua sekaligus)
  t=15: ████████████████████ (semua sekaligus)

Full jitter (cap=5s): tersebar merata
  t=1:  ████
  t=2:  ████████
  t=3:  █████████
  t=4:  ████████
  t=5:  █████

Equal jitter (cap=5s, half=2.5s): tersebar dengan minimum
  t=2.5: (tidak ada — floor di 2.5s)
  t=3:   ███████
  t=4:   ████████████
  t=5:   ██████████

Decorrelated jitter: tumbuh organik, pola bervariasi per instance
  Setiap instance punya pola yang berbeda
  Distribusi lebih natural dari real-world variance

Tujuh Use Case Jitter di Production #

1. Retry dan Exponential Backoff #

Use case paling fundamental. Tanpa jitter, exponential backoff hanya menunda thundering herd — semua client masih retry pada waktu yang sama.

// Retry dengan exponential backoff + full jitter — pola production standar
func retryWithJitter(ctx context.Context, maxAttempts int, fn func() error) error {
    base := 100 * time.Millisecond
    max := 30 * time.Second

    for attempt := 1; attempt <= maxAttempts; attempt++ {
        err := fn()
        if err == nil { return nil }

        if attempt == maxAttempts { return err }

        // Exponential cap
        cap := base * time.Duration(1<<uint(attempt-1))
        if cap > max { cap = max }

        // Full jitter
        delay := time.Duration(rand.Int63n(int64(cap) + 1))

        log.Infof("attempt %d failed, retrying in %v", attempt, delay)
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    return nil
}

2. Cache TTL dengan Jitter #

Mencegah cache stampede ketika banyak entries expired bersamaan.

// Cache TTL dengan jitter — mencegah stampede
type CacheConfig struct {
    BaseTTL    time.Duration
    JitterFrac float64 // 0.0–1.0, fraksi dari BaseTTL sebagai jitter range
}

func (c *CacheConfig) TTL() time.Duration {
    jitterRange := time.Duration(float64(c.BaseTTL) * c.JitterFrac)
    jitter := time.Duration(rand.Int63n(int64(jitterRange*2)+1)) - jitterRange
    ttl := c.BaseTTL + jitter
    if ttl < c.BaseTTL/2 { // floor: minimal 50% dari base TTL
        return c.BaseTTL / 2
    }
    return ttl
}

// Usage: TTL = 1 jam ± 5 menit
cacheConfig := &CacheConfig{
    BaseTTL:    time.Hour,
    JitterFrac: 0.083, // ±5 menit dari 60 menit
}

func setCacheWithJitter(key string, value interface{}) {
    ttl := cacheConfig.TTL()
    redis.Set(key, value, ttl) // setiap key punya TTL sedikit berbeda
}

3. Polling dan Scheduler #

Instance yang berjalan paralel perlu polling tanpa bersamaan.

// Config poller dengan jitter — mencegah polling bersamaan
type PollerConfig struct {
    Interval time.Duration
    Jitter   time.Duration
}

func (p *PollerConfig) NextPollDelay() time.Duration {
    return p.Interval + time.Duration(rand.Int63n(int64(p.Jitter)+1))
}

func (s *FeatureFlagPoller) Run(ctx context.Context) {
    cfg := &PollerConfig{
        Interval: 30 * time.Second,
        Jitter:   10 * time.Second, // delay antara 30–40 detik
    }

    for {
        s.refreshFlags()
        delay := cfg.NextPollDelay()
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            return
        }
    }
}

4. Heartbeat dan Health Check #

// Heartbeat dengan jitter kecil — mencegah spike pada monitoring
type HeartbeatSender struct {
    endpoint string
    base     time.Duration
    jitter   time.Duration
}

func (h *HeartbeatSender) Run(ctx context.Context) {
    for {
        h.send()
        // Base 10s ± 1s — ribuan instance tidak heartbeat bersamaan
        delay := h.base + time.Duration(rand.Int63n(int64(h.jitter*2)+1)) - h.jitter
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            return
        }
    }
}

5. Message Queue Consumer — Visibility Timeout #

Ketika consumer gagal, SQS/RabbitMQ akan membuat pesan muncul kembali. Tanpa jitter pada visibility timeout, banyak consumer akan mencoba memproses ulang pesan yang sama bersamaan.

// Visibility timeout dengan jitter — mencegah consumer retry bersamaan
func calculateVisibilityTimeout(attempt int) int32 {
    base := 30 // detik
    // Tambahkan jitter berdasarkan attempt count
    jitter := rand.Intn(attempt*5 + 5)
    timeout := int32(base + attempt*15 + jitter)
    if timeout > 43200 { // max 12 jam
        timeout = 43200
    }
    return timeout
}

6. Distributed Lock dan Leader Election #

Mencegah contention ketika banyak node mencoba acquire lock bersamaan.

// Leader election dengan jitter — mencegah semua node coba acquire bersamaan
type LeaderElector struct {
    redis       *redis.Client
    lockKey     string
    lockTTL     time.Duration
    retryBase   time.Duration
}

func (e *LeaderElector) TryBecomeLeader(ctx context.Context) bool {
    for {
        // Coba acquire lock
        acquired, err := e.redis.SetNX(ctx, e.lockKey, nodeID, e.lockTTL).Result()
        if err == nil && acquired {
            return true // Berhasil jadi leader
        }

        // Gagal — tunggu dengan jitter sebelum coba lagi
        // Jitter memastikan semua node tidak coba bersamaan
        jitter := time.Duration(rand.Int63n(int64(e.retryBase*3))) + e.retryBase
        select {
        case <-time.After(jitter):
        case <-ctx.Done():
            return false
        }
    }
}

7. Startup Jitter — Mencegah Init Bersamaan #

Ketika banyak instance di-deploy atau restart bersamaan (rolling update, autoscaling), mereka bisa melakukan operasi init yang berat secara bersamaan.

// Startup jitter — menyebar waktu startup untuk mengurangi beban awal
func startupWithJitter(maxJitter time.Duration) {
    // Setiap instance menunggu random duration sebelum mulai
    delay := time.Duration(rand.Int63n(int64(maxJitter) + 1))
    log.Infof("startup delay: %v (anti-thundering-herd)", delay)
    time.Sleep(delay)
}

func main() {
    // Dalam deployment Kubernetes dengan 50 replicas,
    // tanpa startup jitter semua pod melakukan cache warmup bersamaan
    // Dengan jitter 0–30s, warmup tersebar dalam 30 detik
    if os.Getenv("ENABLE_STARTUP_JITTER") == "true" {
        startupWithJitter(30 * time.Second)
    }

    // ... lanjutkan startup normal
    initCache()
    startServer()
}

Jitter untuk Rate Limiting Response #

Ketika server merespons 429 Too Many Requests, server sering menyertakan Retry-After header. Namun jika banyak client menerima 429 pada waktu yang sama, mereka semua akan retry tepat setelah Retry-After — menciptakan thundering herd baru.

// Tambahkan jitter ke Retry-After header
func getRetryAfterWithJitter(resp *http.Response) time.Duration {
    base := parseRetryAfter(resp) // parse header Retry-After

    // Tambahkan jitter ±10% dari base
    jitterRange := base / 10
    jitter := time.Duration(rand.Int63n(int64(jitterRange*2)+1)) - jitterRange

    return base + jitter
}

Checklist Penggunaan Jitter #

Jitter WAJIB digunakan ketika:
  □ Ada retry mechanism yang dijalankan oleh banyak instance serentak
  □ Cache TTL di-set untuk banyak entries pada waktu yang hampir sama
  □ Polling atau scheduler berjalan di banyak instance paralel
  □ Heartbeat dikirim dari ribuan instance ke monitoring server
  □ Leader election atau distributed lock dengan banyak kandidat
  □ Instance baru di-deploy dalam jumlah besar (rolling update, autoscaling)
  □ Consumer queue yang semua instance-nya mengalami kegagalan bersamaan

Jitter tidak diperlukan ketika:
  □ Hanya ada satu instance yang berjalan
  □ Operasi tidak berulang (one-shot)
  □ Timing yang precise dibutuhkan (real-time system dengan constraint ketat)
  □ Interval sudah di-stagger secara eksplisit melalui mekanisme lain

Anti-Pattern yang Harus Dihindari #

// ✗ Jitter yang terlalu kecil — tidak efektif
delay := baseDelay + time.Duration(rand.Intn(10))*time.Millisecond
// Jika baseDelay = 5 detik, jitter 0–10ms tidak berarti apa-apa
// ✓ Jitter minimal 10–20% dari base delay

// ✗ Menggunakan rand.Intn tanpa seed di Go versi lama
// Go 1.20+ otomatis random seed, tapi versi lama perlu explicit seed
rand.Seed(time.Now().UnixNano()) // deprecated di Go 1.20+
// ✓ Gunakan rand.New(rand.NewSource(seed)) untuk reproducibility di test

// ✗ Jitter tanpa floor — delay bisa 0 (sama dengan tanpa backoff)
delay := time.Duration(rand.Int63n(int64(cap)))
// ✓ Untuk equal jitter atau kasus tertentu, tambahkan floor minimal
delay := cap/2 + time.Duration(rand.Int63n(int64(cap/2)))

// ✗ Backoff tanpa jitter — thundering herd tetap terjadi
for attempt := 1; attempt <= maxAttempts; attempt++ {
    fn()
    time.Sleep(baseDelay * time.Duration(1<<attempt)) // semua instance delay sama!
}
// ✓ Selalu tambahkan jitter ke exponential backoff

Ringkasan #

  • Jitter adalah randomisasi waktu yang mencegah banyak instance bergerak dengan ritme identik — solusi untuk thundering herd problem.
  • Thundering herd terjadi ketika banyak instance gagal bersamaan, retry bersamaan, dan menghantam downstream yang baru mulai pulih — jitter memecah sinkronisasi ini.
  • Empat jenis: full jitter (distribusi paling merata, default untuk retry), equal jitter (ada floor minimum), decorrelated jitter (tumbuh organik, AWS-style), fixed interval + small jitter (untuk heartbeat).
  • Full jitter adalah pilihan terbaik untuk retry — distribusi uniform memastikan tidak ada gelombang retry yang bersamaan.
  • Cache TTL jitter mencegah cache stampede ketika banyak entries dibuat pada waktu yang sama.
  • Polling, heartbeat, dan scheduler di lingkungan multi-instance selalu butuh jitter untuk mencegah spike periodik.
  • Leader election dan distributed lock butuh jitter agar node tidak berlomba bersamaan setiap kali lock dilepas.
  • Startup jitter penting untuk rolling deployment — menyebar operasi init yang berat dari banyak instance baru.
  • Jitter tanpa backoff tidak optimal — keduanya bekerja bersama: backoff menentukan skala delay, jitter menyebarkan timing.

← Sebelumnya: Big O   Berikutnya: System Integration →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact