the stack overflowed

Memory & RAM Management in RabbitMQ: Deep Dive

What You'll Learn


Part 1: The Memory Management Problem

Why Memory Management Matters

Scenario: RabbitMQ receives 1 million messages rapidly

Without smart memory management:
  Time 0s:    1 million messages → All in RAM
  Time 1s:    RAM fills up completely
  Time 2s:    No more RAM available
  Time 3s:    RabbitMQ crashes (out of memory)
  
With smart memory management:
  Time 0s:    1 million messages arrive
  Time 1s:    First 100k messages in RAM, rest on disk
  Time 2s:    Consumers process RAM messages
  Time 3s:    As RAM frees, RabbitMQ pages messages from disk
  Time 4s:    System keeps running smoothly

RabbitMQ's memory management is critical for stability in high-throughput scenarios.

The Fundamental Problem

Every message in a queue has two representations:

Message Data = {
  Metadata:      routing_key, timestamp, correlation_id, etc. (~100 bytes)
  Content:       actual message body (1KB - 1GB each)
  State:         is_acked, consumer_info, priority, etc. (~50 bytes)
}

Storing millions of messages in RAM:

Solution: Move messages to disk (slower but has unlimited space), keep only hot messages in RAM.


Part 2: The Three-Tier Storage Model

Tier 1: RAM (Hot Messages)

Where it is: In-memory queue (linked list or similar)

How fast: Microseconds to access

How much: Limited (typical: 1-4GB per node)

Purpose: Active messages being processed by consumers

Queue in RAM = [Message1, Message2, Message3, ... MessageN]
               ↑
            Consumers pull from here (fast!)

Tier 2: Page Store (Warm Messages)

Where it is: Memory-mapped files on disk (special files)

How fast: Milliseconds to access (disk I/O)

How much: Depends on disk space available

Purpose: Messages waiting for consumers (queue backup)

Queue (spilled to disk) = [...PagedMessages...]
                          ↑
                     When RAM fills up, messages move here

Tier 3: Queue Index (Message Metadata Only)

Where it is: Disk + In-memory index

How fast: Fast for lookups (in-memory), slow for writes (disk)

How much: Much smaller than full messages (metadata only)

Purpose: Track message positions and state


Part 3: The Paging Mechanism

When Does Paging Happen?

RabbitMQ monitors memory usage and pages messages to disk when:

  1. Memory Threshold Exceeded

    • Default: 40% of available RAM
    • Example: On a system with 16GB RAM, threshold is ~6.4GB
  2. Queue Specific Limit

    • Queue can declare max in-memory messages
    • Excess goes to disk automatically
  3. Node Memory Pressure

    • If entire node is under memory pressure
    • Page aggressively to free RAM

The Paging Flow

Message arrives → Queue receives it
                    ↓
              Check: Is RAM usage < threshold?
                    ↓
        ┌───────────┴─────────────┐
        ↓                         ↓
      YES                        NO
        ↓                         ↓
   ┌─────────────┐      ┌──────────────────┐
   │ Store in    │      │ Write to disk    │
   │ RAM Queue   │      │ (page store)     │
   └─────────────┘      └──────────────────┘
        ↓                         ↓
   Store metadata         Store metadata
   in memory index        in memory index
        ↓
   Ready for fast access

The Paging Algorithm: LRU-ish

RabbitMQ doesn't use pure LRU (Least Recently Used), but something similar:

When memory pressure increases:

1. Page out oldest messages first
   (Messages that arrived earliest)

2. Keep newest messages in RAM
   (Recently arrived = more likely to be consumed soon)

3. Consumers prefer messages from RAM
   (If message is on disk, pull it in, evict older RAM message)

Pseudo-algorithm:

Page messages until memory usage < threshold:
  For each queue (in some order):
    While queue has paged messages:
      Take oldest message
      Write to disk (page store)
      Remove from RAM
      Update index

Part 4: Memory Lifecycle of a Single Message

High-Level Overview

Message Lifecycle:

1. ARRIVAL (RAM)
   └─ Producer publishes → Message in RAM queue
   
2. WAITING (RAM or DISK)
   └─ If memory pressure: → Page to disk
   
3. DELIVERY (RAM)
   └─ Consumer requests → Load from disk if needed
   
4. ACKNOWLEDGMENT (FREED)
   └─ Consumer acks → Remove from queue entirely

Detailed Timeline: Non-Durable Message

TIME    EVENT                          LOCATION      MEMORY
────────────────────────────────────────────────────────────

T0      Producer publishes message
        ("hello world", 11 bytes)      

T1      RabbitMQ receives             RAM           +11 bytes
        Message stored in queue
        Index entry created
        
T2      No consumer waiting           RAM           (same)
        Message sits in queue

T3      Memory at 50% usage          RAM           (same)
        No paging triggered

T4      Another 100k messages         RAM           +100MB
        arrive rapidly

T5      Memory now at 95% (paging     RAM/DISK      (same RAM,
        threshold exceeded!)                         +disk space)

T6      Paging starts
        Our message: "hello world"    DISK          -11 bytes RAM
        → Written to page store                     +11 bytes DISK

T7      Another consumer request      RAM           (same RAM,
        arrives                                     loads from DISK)

T8      Message loaded from disk      RAM           +11 bytes RAM
        into consumer's buffer                      (evict another
                                                     message)

T9      Consumer processes message    RAM           (processing)

T10     Consumer sends ACK            RAM           (nothing)

T11     Message removed from queue    -             -11 bytes RAM
        Acknowledgment recorded
        Message can be garbage
        collected

Detailed Timeline: Durable Message

TIME    EVENT                          LOCATION      STORAGE
────────────────────────────────────────────────────────────

T0      Producer publishes            
        Durable=true

T1      RabbitMQ receives             RAM+DISK      +11 bytes RAM
        Message → RAM queue                         +11 bytes DISK
        + Write to persistent store                 (wal log)

T2      Confirmation sent to          RAM+DISK      (same)
        producer only after
        disk write completes

T3-5    (same as non-durable)         RAM+DISK      (same)

T6      Paging occurs                 RAM+DISK      -11 bytes RAM
        Message already on disk                     (same disk -
                                                     was already there)

T7      Message paged from             DISK only    (already on disk,
        RAM (no redundant write)                    so faster)

T8      Message loaded from disk      RAM+DISK      +11 bytes RAM
        into consumer buffer

T9-11   (same as non-durable)         RAM+DISK      -11 bytes RAM
                                                    -11 bytes DISK
        Both removed when acked

Part 5: Understanding Memory Thresholds

RabbitMQ Memory Configuration

% In /etc/rabbitmq/rabbitmq.conf

% Method 1: Memory threshold (relative)
vm_memory_high_watermark.relative = 0.6
% Meaning: Page when RAM usage exceeds 60% of available

% Method 2: Memory threshold (absolute)
vm_memory_high_watermark.absolute = 2GB
% Meaning: Page when RAM usage exceeds 2GB

% Method 3: Multiple thresholds for gradual paging
vm_memory_high_watermark.relative = 0.6
% 60% = start paging
% 50% = moderate paging
% 40% = aggressive paging (if memory keeps rising)

Memory Threshold Stages

RabbitMQ actually has multiple thresholds (not just one):

100% ┌────────────────────────────────────
     │ CRITICAL (node starts blocking new messages)
 80% ├────────────────────────────────────
     │ ALARM (aggressive paging, connections suspended)
 60% ├────────────────────────────────────
     │ WARNING (paging active, but still accepting)
 40% ├────────────────────────────────────
     │ NORMAL (no paging)
  0% └────────────────────────────────────

Monitor this with:
rabbitmqctl eval 'erlang:memory().'

Practical Example: 16GB System

# System has 16GB total RAM
# RabbitMQ can use max: ~14GB (leaving 2GB for OS)

# Configuration:
vm_memory_high_watermark.relative = 0.4
# 40% of 14GB = 5.6GB
# When RabbitMQ uses > 5.6GB RAM: START PAGING

# Real-world impact:

Case 1: Light load (2GB in use)
  Status: NORMAL
  Paging: OFF
  Performance:  Excellent (all messages in RAM)

Case 2: Moderate load (5.5GB in use)
  Status: NORMAL  approaching WARNING
  Paging: OFF  Starting
  Performance:  Good (most messages in RAM)

Case 3: Heavy load (8GB in use)
  Status: WARNING
  Paging: ACTIVE
  Performance: ⚠️  Degraded (frequent disk I/O)

Case 4: Overload (13GB in use)
  Status: ALARM  CRITICAL
  Paging: AGGRESSIVE
  Performance:  Bad (most messages on disk, frequent seeks)
  New producers: BLOCKED (can't send more messages)

Part 6: Queue-Level Memory Limits

Per-Queue Memory Policies

Not all messages in all queues are equally important. You can set per-queue policies:

const amqp = require('amqplib');

async function setupQueueWithMemoryPolicy() {
  const conn = await amqp.connect('amqp://localhost');
  const ch = await conn.createChannel();
  
  // Queue with memory limit
  await ch.assertQueue('high-priority-queue', {
    durable: true,
    arguments: {
      // Max 100MB for this queue only
      'x-max-length-bytes': 100 * 1024 * 1024,
      
      // OR max 10,000 messages
      'x-max-length': 10000,
      
      // What to do when limit exceeded?
      // 'drop-head': Remove oldest (default)
      // 'reject-publish': Reject new messages
      'x-overflow': 'drop-head'
    }
  });
  
  console.log('Queue with memory limit created');
}

How Queue Memory Limits Work

Queue: "orders"
Memory limit: 100MB
Current size: 90MB

Message arrives (11 bytes):
  Step 1: Check if adding message exceeds limit
          90MB + 11 bytes = 90MB + 0.000011MB < 100MB ✓
  
  Step 2: Add message
          Queue size: 90.000011MB

[Later...]

Queue: "orders"
Memory limit: 100MB
Current size: 99.999MB

Message arrives (10MB):
  Step 1: Check if adding message exceeds limit
          99.999MB + 10MB > 100MB ✗ LIMIT EXCEEDED
  
  Step 2: x-overflow = 'drop-head'
          Remove oldest message (was 5MB)
  
  Step 3: Queue size: 99.999 - 5 + 10 = 104.999MB
          Wait, still over! Remove another message (8MB)
  
  Step 4: Queue size: 96.999MB < 100MB ✓
  
  Step 5: Add new message
          Queue size: 106.999MB
          
Wait, that exceeds 100MB!
Actually, RabbitMQ removes UNTIL new message fits, allowing overflow:
  99.999 - 5 - 8 + 10 = 96.999MB
  But some implementations allow the overflow

x-overflow: 'reject-publish'

If queue at limit: reject the new message immediately
Producer gets error: 406 PRECONDITION_FAILED
Queue stays at/under limit

Part 7: Durability vs. Memory Usage

Non-Durable Queue

await ch.assertQueue('transient-queue', {
  durable: false  // ← Not persistent
});

const msg = { data: 'important work' };
ch.sendToQueue('transient-queue', Buffer.from(JSON.stringify(msg)));

Memory impact:

Message only exists in RAM queue
No disk writes
No WAL (write-ahead log) entries
Memory freed immediately when acked

If RabbitMQ crashes:
  Message is GONE forever
  
Fast but risky:
  ✓ Fast publish (no disk I/O)
  ✓ Low memory overhead (no persistent store)
  ✗ Data loss on crash

When to use:

Durable Queue

await ch.assertQueue('persistent-queue', {
  durable: true  // ← Persistent
});

const msg = { data: 'important transaction' };
ch.sendToQueue('persistent-queue', Buffer.from(JSON.stringify(msg)), {
  persistent: true  // ← Both queue AND message
});

Memory impact:

Message written to:
  1. RAM queue (fast access)
  2. Disk (Write-Ahead Log / persistent store)

Actually two copies exist temporarily:
  Memory: ~11 bytes/message
  Disk: ~11 bytes/message
  Total: ~22 bytes per message

RabbitMQ only confirms to producer after disk write.
Slower but safer.

If RabbitMQ crashes:
  Messages recovered from disk
  No data loss
  
When RabbitMQ restarts:
  Loads durable queues from disk
  Can take minutes if millions of messages

When to use:

The Durability-Memory Tradeoff

Message Durability:      Memory Usage:           Disk Usage:
─────────────────────────────────────────────────────────────

Non-durable              Minimal                 None
  Message in RAM only    (just message)          (messages lost
                                                  on crash)

Durable                  Doubled initially       Full message
  Message in RAM+Disk    (RAM + disk while      stored to disk
                         writing, then just
                         RAM after page)

Example: 1 million messages, 1KB each
─────────────────────────────────────────────────────────────
Non-durable: ~1GB RAM max
Durable: ~1GB RAM + ~1GB Disk writes + paging

Part 8: The Write-Ahead Log (WAL)

What is the WAL?

The Write-Ahead Log is how RabbitMQ ensures durability:

Producer publishes durable message:

1. Message received
2. Before keeping in RAM queue...
3. Write to disk: "queue-x, message-y, [data]" ← WAL
4. Flush to disk (fsync)
5. NOW add to RAM queue
6. Confirm to producer: "message persisted"

Key principle: Write to disk FIRST, RAM SECOND. This ensures data survives crashes.

WAL Location and Structure

/var/lib/rabbitmq/mnesia/[node-name]/queues/

# Directory structure:
├── queue-1-seg-1  # Segment 1 of queue-1 WAL
├── queue-1-seg-2  # Segment 2 of queue-1 WAL
├── queue-1.idx    # Index file (metadata)
├── queue-2-seg-1
├── queue-2-seg-2
└── ...

Inside a segment file:

[Header] [Entry1] [Entry2] [Entry3] ... [Entry1000]

Entry format:
  ┌──────────────────────────────┐
  │ Timestamp   │ Message data   │
  │ (8 bytes)   │ (variable)     │
  │             │                │
  │ Checksum    │ Flags          │
  │ (4 bytes)   │ (1 byte)       │
  └──────────────────────────────┘

WAL Performance Impact

const ch = await conn.createChannel();

// Non-durable: Just RAM
console.time('non-durable publish');
for (let i = 0; i < 10000; i++) {
  ch.sendToQueue('temp-queue', Buffer.from('msg'));
}
console.timeEnd('non-durable publish');
// Output: ~50ms (super fast, no disk I/O)

// Durable: RAM + Disk WAL
console.time('durable publish');
for (let i = 0; i < 10000; i++) {
  ch.sendToQueue('durable-queue', Buffer.from('msg'), 
    { persistent: true });
}
console.timeEnd('durable publish');
// Output: ~5000ms (100x slower! Disk I/O overhead)

Why so much slower?

Modern optimization: Batch writes

Write 10 messages to WAL:
  Old way: 10 separate fsync calls = ~70ms
  Batch way: 1 fsync call for all 10 = ~7ms
  
RabbitMQ batches by default, so actual: ~500ms
(Still 10x slower than non-durable, but acceptable)

Part 9: Monitoring Memory Usage

RabbitMQ CLI Tools

# Get memory statistics
rabbitmqctl eval 'erlang:memory().'

# Output:
# {total,4729034},
#  {processes,1840101},
#  {processes_used,1840101},
#  {system,2888933},
#  {atom,523421},
#  {atom_used,507451},
#  {binary,123456},
#  {code,7891234},
#  {ets,456789}

# Interpret:
# total = 4.7MB total memory
# processes = Erlang processes
# system = System allocations
# code = Compiled code in memory
# ets = Erlang Term Storage (queues, etc.)

Management API

# Get node memory details
curl -s http://localhost:15672/api/nodes \
  -u guest:guest | jq '.[] | {name, memory_used, memory_limit}'

# Output:
# {
#   "name": "rabbit@localhost",
#   "memory_used": 8453234,     # 8.4MB
#   "memory_limit": 14532424,   # 14.5MB (40% of 16GB)
#   "memory_alarm": false
# }

# Get queue memory usage
curl -s http://localhost:15672/api/queues \
  -u guest:guest | jq '.[] | {name, memory, messages}'

# Output:
# [
#   {
#     "name": "orders",
#     "memory": 1234567,        # 1.2MB
#     "messages": 5000          # 5000 messages in queue
#   },
#   {
#     "name": "notifications",
#     "memory": 567890,         # 567KB
#     "messages": 1000
#   }
# ]

Memory Per Message Calculation

# Get queue stats
QUEUE_NAME="orders"

MEMORY=$(curl -s "http://localhost:15672/api/queues/%2F/${QUEUE_NAME}" \
  -u guest:guest | jq '.memory')

MESSAGES=$(curl -s "http://localhost:15672/api/queues/%2F/${QUEUE_NAME}" \
  -u guest:guest | jq '.messages')

BYTES_PER_MSG=$((MEMORY / MESSAGES))

echo "Queue: $QUEUE_NAME"
echo "Total Memory: $MEMORY bytes"
echo "Message Count: $MESSAGES"
echo "Bytes per Message: $BYTES_PER_MSG"

# Example output:
# Queue: orders
# Total Memory: 1234567 bytes
# Message Count: 5000
# Bytes per Message: 247  (includes overhead!)

Monitoring for Memory Pressure

# Check if memory alarm is active
rabbitmqctl status | grep memory_alarm

# If memory_alarm is true:
# RabbitMQ has hit the high watermark
# Paging is active
# New publishers may be blocked

# Check memory threshold
rabbitmqctl eval 'rabbit:vm_memory_monitor_get_watermark().'
# Returns: {relative, 0.4} or similar

# Real-time monitoring
watch -n 1 "rabbitmqctl eval 'erlang:memory().' | grep total"

Part 10: Tuning Memory Usage

Strategy 1: Adjust Memory Thresholds

# /etc/rabbitmq/rabbitmq.conf

# More aggressive paging (start earlier)
vm_memory_high_watermark.relative = 0.3
# Start paging at 30% instead of 40%
# Pro: Prevents memory exhaustion
# Con: More disk I/O, slower

# Less aggressive paging (start later)
vm_memory_high_watermark.relative = 0.5
# Start paging at 50%
# Pro: Keep more in RAM, faster
# Con: Risk of OOM if traffic spikes

Strategy 2: Use Non-Durable Queues for Non-Critical Data

// For temporary work (don't need durability)
await ch.assertQueue('temp-processing', { durable: false });

// For critical data (need durability)
await ch.assertQueue('orders', { durable: true });

Memory impact:

Strategy 3: Set Queue Memory Limits

// Prevent single queue from consuming all memory
await ch.assertQueue('user-events', {
  durable: true,
  arguments: {
    'x-max-length-bytes': 500 * 1024 * 1024,  // 500MB max
    'x-overflow': 'drop-head'
  }
});

Strategy 4: Optimize Message Size

// ❌ Large message (5KB)
const msg = {
  userId: 123,
  userName: 'John Doe',
  email: 'john@example.com',
  fullProfile: {...large object...},  // Lots of unnecessary data
  timestamp: Date.now()
};
ch.sendToQueue('users', Buffer.from(JSON.stringify(msg)));

// ✅ Optimized message (500 bytes)
const msg = {
  userId: 123,
  timestamp: Date.now()
};
ch.sendToQueue('users', Buffer.from(JSON.stringify(msg)));
// Consumer can fetch full profile from DB if needed

Memory savings:

Strategy 5: Consumer Speed Tuning

const ch = await conn.createChannel();

// ❌ Slow consumer (bottleneck)
ch.prefetch(1);  // Only 1 message at a time
ch.consume('orders', async (msg) => {
  await slowDatabaseInsert(msg);  // Takes 1 second per message
  ch.ack(msg);
});
// Processing 1 message/sec
// 1 million messages = 11.5 days!

// ✅ Fast consumer (keeps up)
ch.prefetch(100);  // 100 messages at a time
ch.consume('orders', async (msg) => {
  // Same slow DB insert, but...
  fastBatchInsert([messages]);  // Batch inserts (faster)
  ch.ack(msg);
});
// Processing 100 messages/sec
// 1 million messages = ~2.8 hours

Memory impact:

Strategy 6: Use Lazy Queues

// Classic queue (eager) - keeps messages in RAM
await ch.assertQueue('eager-queue', {
  durable: true,
  arguments: {
    'x-queue-type': 'classic'
  }
});

// Lazy queue - keeps messages on disk until accessed
await ch.assertQueue('lazy-queue', {
  durable: true,
  arguments: {
    'x-queue-type': 'lazy'
  }
});

Lazy queue behavior:

Classic (Eager):
  Message arrives → Stored in RAM → Paged to disk if memory pressure

Lazy:
  Message arrives → Stored on disk immediately → Loaded to RAM when needed

Lazy queue memory: 95% lower than classic!
Classic queue latency: 95% lower than lazy!

Trade-off: Pick based on your priority
  Critical low-latency: Use classic queues
  High-volume buffering: Use lazy queues

Part 11: Real-World Scenarios

Scenario 1: Memory Leak Diagnosis

# Symptoms:
# - Memory grows every hour
# - Paging increases
# - Eventually runs out of memory

# Investigation:
curl -s http://localhost:15672/api/queues \
  -u guest:guest | jq 'sort_by(.memory) | reverse | .[:5]'

# Output: Top 5 queues by memory usage
# Check if one queue is growing endlessly

# Diagnosis:
# Consumers not acknowledging messages
# Messages accumulate in queue
# Memory keeps growing

# Fix:
# 1. Check consumer code for missing ch.ack()
# 2. Check for long-running consumer (blocked on I/O)
# 3. Add consumer timeout

Scenario 2: Handling a Traffic Spike

Normal traffic: 100 messages/sec
Suddenly: 10,000 messages/sec (spike)

Without memory tuning:
  T0: 100MB/sec queued
  T1: RAM fills
  T2: Paging starts (slow)
  T3: Memory alarm triggered
  T4: New publishers blocked
  T5: Cascade failure

With memory tuning:
  T0: Pre-configured memory limit: 500MB per queue
  T1: Excess messages dropped (x-overflow: drop-head)
  T2: System stays responsive
  T3: Alerting triggered, engineers notified
  T4: Auto-scaling adds RabbitMQ nodes
  T5: Spike absorbed, no cascade failure

Scenario 3: Gradual Memory Growth

# Monitor over 24 hours

# Hour 0
rabbitmqctl eval 'erlang:memory(total).'
# {total, 1073741824}  # 1GB

# Hour 6
rabbitmqctl eval 'erlang:memory(total).'
# {total, 2147483648}  # 2GB (doubles!)

# Hour 12
# {total, 3221225472}  # 3GB

# Hour 24
# {total, 5368709120}  # 5GB

# Problem: Memory grows 200MB per hour
# Likely causes:
# 1. Connection leaks (new connections not closing)
# 2. Channel leaks (channels not closing)
# 3. Queue accumulation (messages not being consumed)
# 4. Internal memory fragmentation (Erlang GC issue)

# Investigation:
rabbitmqctl list_connections | wc -l    # Check connection count
rabbitmqctl list_channels | wc -l         # Check channel count
curl -s http://localhost:15672/api/queues -u guest:guest | \
  jq '[.[] | select(.messages > 0)] | length'  # Non-empty queues

Complete Memory Configuration

# /etc/rabbitmq/rabbitmq.conf

# Memory threshold (start paging)
vm_memory_high_watermark.relative = 0.4

# Enable flow control (block publishers when memory high)
credit_flow_default_credit = 400

# Page cache size
vm_memory_monitor_interval = 5000  # milliseconds

# Lazy queue default (keep on disk)
queue_master_locator = min-masters

# Paging segment size (page store files)
# Larger = fewer files, less overhead
# Smaller = more granular paging
disk_free_limit.relative = 0.1

# Maximum file descriptor limit
nofile = 65535

Memory Monitoring Interval

# How often does RabbitMQ check memory?
vm_memory_monitor_interval = 5000  # milliseconds

# If memory spikes to 100% in < 5 seconds:
# May not catch it in time!

# For bursty traffic, reduce interval:
vm_memory_monitor_interval = 1000  # Check every 1 second

Key Takeaways

Memory Management Summary

  1. Three-Tier Model

    • RAM: Hot messages, microsecond access
    • Page Store: Warm messages, millisecond access
    • Queue Index: Metadata for quick lookups
  2. Paging Mechanism

    • Triggered at memory threshold (default 40%)
    • Moves old messages to disk automatically
    • Reloads on access (LRU-ish)
  3. Durability vs. Memory

    • Non-durable: Fast, low memory, risky
    • Durable: Safe, higher memory, slower
  4. Message Lifecycle

    • Arrival → RAM queue
    • Memory pressure → Paged to disk
    • Consumer request → Loaded back to RAM
    • ACK → Removed from both
  5. Monitoring

    • Check erlang:memory() for total
    • Use Management API for queue details
    • Watch for memory alarms
    • Track trends over time
  6. Tuning Strategies

    • Adjust thresholds for your workload
    • Use non-durable for temporary data
    • Set per-queue memory limits
    • Optimize message size
    • Improve consumer speed
    • Consider lazy queues for buffering
  7. Common Issues

    • Memory leaks from unclosed connections/channels
    • Queue accumulation from slow consumers
    • Threshold too low (excessive paging)
    • Threshold too high (OOM risk)
  8. Performance Implications

    • Messages in RAM: Microseconds latency
    • Messages on disk (paged): Milliseconds latency
    • Under memory pressure: 10-100x slower
    • OOM scenario: System becomes unresponsive

When to Worry

Good:

⚠️ Investigate:

Critical: