Sidekiq ๐Ÿฆฟ Deep Dive: The Ruby Background Job ๐Ÿ‘ท๐Ÿฝโ€โ™‚๏ธ Processor That Powers Modern Rails Applications

Background job processing is a cornerstone of modern web applications, and in the Ruby ecosystem, one library has dominated this space for over a decade: Sidekiq. Whether you’re building a simple Rails app or a complex distributed system, chances are you’ve encountered or will encounter Sidekiq. But how does it actually work under the hood, and why has it remained the go-to choice for Ruby developers?

๐Ÿ” What is Sidekiq?

Sidekiq is a Ruby background job processor that allows you to offload time-consuming tasks from your web application’s request-response cycle. Instead of making users wait for slow operations like sending emails, processing images, or calling external APIs, you can queue these tasks to be executed asynchronously in the background.

# Instead of this blocking the web request
UserMailer.welcome_email(user).deliver_now

# You can do this
UserMailer.welcome_email(user).deliver_later

โค๏ธ Why Ruby Developers Love Sidekiq

โšก Battle-Tested Reliability

With over 10 years in production and widespread adoption across the Ruby community, Sidekiq has proven its reliability in handling millions of jobs across thousands of applications.

๐Ÿงต Efficient Threading Model

Unlike many other Ruby job processors that use a forking model, Sidekiq uses threads. This makes it incredibly memory-efficient since threads share the same memory space, allowing you to process multiple jobs concurrently with minimal memory overhead.

๐Ÿš„ Redis-Powered Performance

Sidekiq leverages Redis’s lightning-fast data structures, using simple list operations (BRPOP, LPUSH) that provide constant-time complexity for job queuing and dequeuing.

๐Ÿ”ง Simple Integration

For Rails applications, integration is often as simple as adding the gem and configuring a few settings. Sidekiq works seamlessly with ActiveJob, Rails’ job interface.

๐ŸŒ Rich Ecosystem

The library comes with a web UI for monitoring jobs, extensive configuration options, and a thriving ecosystem of plugins and extensions.

๐Ÿ”„ Alternatives to Sidekiq

While Sidekiq dominates the Ruby job processing landscape, several alternatives exist:

  • Resque: The original Redis-backed job processor for Ruby, uses a forking model
  • DelayedJob: Database-backed job processor, simpler but less performant
  • Que: PostgreSQL-based job processor using advisory locks
  • GoodJob: Rails-native job processor that stores jobs in PostgreSQL
  • Solid Queue: Rails 8′s new default job processor (though Sidekiq remains popular)

However, Sidekiq’s combination of performance, reliability, and ecosystem support keeps it as the preferred choice for most production applications.

๐Ÿ“… Is Sidekiq Getting Old?

Far from it! Sidekiq continues to evolve actively:

  • Regular Updates: The library receives frequent updates and improvements
  • Rails 8 Compatibility: Sidekiq works perfectly with the latest Rails versions
  • Modern Ruby Support: Supports Ruby 3.x features and performance improvements
  • Active Community: Strong maintainer support and community contributions

The core design principles that made Sidekiq successful (threading, Redis, simplicity) remain as relevant today as they were a decade ago.

โš™๏ธ How Sidekiq Actually Works

Let’s dive into the technical architecture, drawing from Dan Svetlov’s excellent internals analysis.

๐Ÿš€ The Boot Process

  1. CLI Initialization: Sidekiq starts via bin/sidekiq, which creates a Sidekiq::CLI instance
  2. Configuration Loading: Parses YAML config files and command-line arguments
  3. Application Loading: Requires your Rails application or specified Ruby files
  4. Signal Handling: Sets up handlers for SIGTERM, SIGINT, SIGTTIN, and SIGTSTP

๐Ÿ—๏ธ The Core Architecture

# Simplified Sidekiq architecture
Manager
โ”œโ”€โ”€ Processor Threads (default: RAILS_MAX_THREADS)
โ”œโ”€โ”€ Poller Thread (handles scheduled/retry jobs)
โ””โ”€โ”€ Fetcher (BasicFetch - pulls jobs from Redis)

๐Ÿ”„ Job Processing Lifecycle

  1. Job Enqueueing: Jobs are pushed to Redis lists using LPUSH
  2. Job Fetching: Worker processes use BRPOP to atomically fetch jobs
  3. Execution: Each job runs in its own thread within a processor
  4. Completion: Successful jobs are simply removed; failed jobs enter retry logic

โœจ The Threading Magic

Here’s the fascinating part: Sidekiq uses a Manager class that spawns multiple Processor threads:

# Conceptual representation
@workers = @concurrency.times.map do
  Processor.new(self, &method(:processor_died))
end

Each processor thread runs an infinite loop, constantly fetching and executing jobs:

def start
  @thread = safe_thread("processor", &method(:run))
end

private

def run
  while !@done
    process_one
  end
rescue Sidekiq::Shutdown
  # Graceful shutdown
end

๐Ÿงต Ruby’s Threading Reality: Debunking the Myth

There’s a common misconception that “Ruby doesn’t support threads.” This isn’t accurate. Ruby absolutely supports threads, but it has an important limitation called the Global Interpreter Lock (GIL).

๐Ÿ”’ What the GIL Means:

  • Only one Ruby thread can execute Ruby code at a time
  • I/O operations release the GIL, allowing other threads to run
  • Most background jobs involve I/O: database queries, API calls, file operations

This makes Sidekiq’s threading model perfect for typical background jobs:

# This job releases the GIL during I/O operations
class EmailJob < ApplicationJob
  def perform(user_id)
    user = User.find(user_id)        # Database I/O - GIL released
    email_service.send_email(user)   # HTTP request - GIL released
    log_event(user)                  # File/DB I/O - GIL released
  end
end

Multiple EmailJob instances can run concurrently because they spend most of their time in I/O operations where the GIL is released.

๐Ÿ—„๏ธ Is Redis Mandatory?

Yes, Redis is absolutely mandatory for Sidekiq. Redis serves as:

  1. Job Storage: All job data is stored in Redis lists and sorted sets
  2. Queue Management: Different queues are implemented as separate Redis lists
  3. Scheduling: Future and retry jobs use Redis sorted sets with timestamps
  4. Statistics: Job metrics and monitoring data live in Redis

The tight Redis integration is actually one of Sidekiq’s strengths:

# Job queuing uses simple Redis operations
redis.lpush("queue:default", job_json)

# Job fetching is atomic
job = redis.brpop("queue:default", timeout: 2)

๐Ÿš€ Sidekiq in a Rails 8 Application

Here’s how Sidekiq integrates beautifully with a modern Rails 8 application:

๐Ÿ“ฆ 1. Installation and Setup

# Gemfile
gem 'sidekiq'

# config/application.rb
config.active_job.queue_adapter = :sidekiq

โš™๏ธ 2. Configuration

# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
  config.concurrency = 5
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

๐Ÿ’ผ 3. Creating Jobs

# app/jobs/user_onboarding_job.rb
class UserOnboardingJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome_email(user).deliver_now
    user.update!(onboarded_at: Time.current)
  end
end

# Enqueue the job
UserOnboardingJob.perform_later(user.id)

๐ŸŽฏ 4. Advanced Features

# Scheduled jobs
UserOnboardingJob.set(wait: 1.hour).perform_later(user.id)

# Job priorities with different queues
class UrgentJob < ApplicationJob
  queue_as :high_priority
end

# Sidekiq configuration for queue priorities
# config/sidekiq.yml
:queues:
  - [high_priority, 3]
  - [default, 2]  
  - [low_priority, 1]

๐Ÿ“Š 5. Monitoring and Debugging

Sidekiq provides a fantastic web UI accessible via:

# config/routes.rb
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'

๐Ÿญ Production Considerations

๐Ÿ›‘ Graceful Shutdown

Sidekiq handles graceful shutdowns elegantly. When receiving SIGTERM (common in Kubernetes deployments):

  1. Stops accepting new jobs
  2. Allows current jobs to complete (with timeout)
  3. Requeues any unfinished jobs back to Redis
  4. Shuts down cleanly

โš ๏ธ Job Loss Scenarios

While Sidekiq provides “at least once” delivery semantics, jobs can be lost in extreme scenarios:

  • Process killed with SIGKILL (no graceful shutdown)
  • Redis memory exhaustion during job requeuing
  • Redis server failures with certain persistence configurations

For mission-critical jobs, consider:

  • Implementing idempotency
  • Adding liveness checks via cron jobs
  • Using Sidekiq Pro for guaranteed job delivery

๐ŸŽฏ Conclusion

Sidekiq remains the gold standard for background job processing in Ruby applications. Its efficient threading model, Redis-powered performance, and seamless Rails integration make it an excellent choice for modern applications. The library’s maturity doesn’t mean stagnation โ€“ it represents battle-tested reliability with continuous evolution.

Whether you’re building a simple Rails 8 application or a complex distributed system, Sidekiq provides the robust foundation you need for handling background work efficiently and reliably.


Want to dive deeper into Sidekiq’s internals? Check out Dan Svetlov’s comprehensive technical analysis that inspired this post.

Questions ๐Ÿง

1. Is Sidekiq heavy?

No, Sidekiq is actually quite lightweight! Here’s why:

Memory Efficiency: Sidekiq uses a threading model instead of forking processes. This is crucial because:

  • Threads share the same memory space
  • Multiple jobs can run concurrently with minimal memory overhead
  • Much more memory-efficient than alternatives like Resque that fork processes

Performance: The blog post mentions that Sidekiq leverages Redis’s lightning-fast operations using simple list operations (BRPOP, LPUSH) with constant-time complexity.

Resource Usage: The default concurrency is typically set to RAILS_MAX_THREADS (usually 5), meaning you get good parallelism without overwhelming your system.

2. Sidekiq vs ActiveJob Relationship

Sidekiq is NOT an alternative to ActiveJob – they work together beautifully:

ActiveJob is Rails’ interface/abstraction layer for background jobs. It provides:

  • A common API for defining jobs
  • Queue adapters for different backends
  • Built-in features like retries, scheduling, etc.

Sidekiq is a queue adapter/backend that actually processes the jobs. The relationship works like this:

# ActiveJob provides the interface
class UserOnboardingJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    # Your job logic here
  end
end

# Sidekiq acts as the backend processor
# config/application.rb
config.active_job.queue_adapter = :sidekiq

Think of it this way:

  • ActiveJob = The standardized job interface (like ActiveRecord for databases)
  • Sidekiq = The actual job processing engine (like PostgreSQL for databases)

When you write UserOnboardingJob.perform_later(user.id), ActiveJob translates this into Sidekiq’s format and queues it in Redis, then Sidekiq processes it.

Other queue adapters you could use with ActiveJob include:

  • :delayed_job
  • :resque
  • :solid_queue (Rails 8’s new default)
  • :que

But Sidekiq remains the most popular choice due to its performance and reliability!


๐ŸŽฏ Why Solid Queue (Rails 8) Was Created

1. Zero External Dependencies

Sidekiq requires Redis, which means:

  • Additional infrastructure to set up and maintain
  • Extra cost on hosting platforms (Heroku Redis add-on costs money)
  • More complexity in deployment and monitoring

Solid Queue uses your existing PostgreSQL database, so:

  • No additional infrastructure needed
  • Every Rails app already has a database
  • Simpler deployment and maintenance
2. Rails-Native Philosophy

The Rails team wanted a solution that’s:

  • Built specifically for Rails by the Rails team
  • Follows Rails conventions and patterns
  • Integrates seamlessly without external dependencies
  • Ships “out of the box” with Rails
3. Simplicity for Smaller Apps

For many Rails applications:

  • Setting up Redis just for background jobs is overkill
  • The job volume doesn’t require Redis-level performance
  • Database-backed jobs are perfectly sufficient
4. Cost and Hosting Considerations
  • Heroku: Adding Redis costs $5-15+ per month extra
  • Smaller projects: May not justify the additional infrastructure cost
  • Development: Easier local development without Redis setup
5. Different Performance Trade-offs

While Sidekiq is faster, Solid Queue offers:

  • ACID guarantees from PostgreSQL
  • Better durability (jobs survive Redis restarts/crashes)
  • Simpler backup/restore (part of your database backup)

๐Ÿค” When to Choose Which?

Choose Solid Queue when:
  • Building smaller to medium Rails apps
  • Want to minimize infrastructure complexity
  • Don’t need extremely high job throughput
  • Cost is a consideration
  • Want Rails-native solution
Choose Sidekiq when:
  • High job volume/throughput requirements
  • Already using Redis in your stack
  • Need advanced features (Sidekiq Pro/Enterprise)
  • Want the most battle-tested solution
  • Performance is critical

๐Ÿ“Š Real-World Impact

# Solid Queue - No Redis needed
# Uses your existing PostgreSQL database
config.active_job.queue_adapter = :solid_queue

# Sidekiq - Requires Redis
# But offers superior performance
config.active_job.queue_adapter = :sidekiq

๐ŸŽฏ The Bottom Line

Solid Queue wasn’t created because Sidekiq is bad – it’s created because:

  1. Different use cases: Not every app needs Redis-level performance
  2. Rails philosophy: “Convention over configuration” includes sensible defaults
  3. Accessibility: Lower barrier to entry for new Rails developers
  4. Infrastructure simplicity: One less moving part to manage

Sidekiq remains excellent and is still widely used in production. Many companies will continue using Sidekiq, especially for high-traffic applications.

Think of it like this:

  • Solid Queue = The sensible, zero-dependency default (like SQLite for development)
  • Sidekiq = The high-performance, battle-tested option (like PostgreSQL for production)

Both have their place in the ecosystem! The Rails team just wanted to provide a great default option that doesn’t require additional infrastructure setup.


๐Ÿš€ What Happens When You Run bin/sidekiq

1. Command Execution

$ bin/sidekiq

This executes the Sidekiq binary, which typically looks like this:

#!/usr/bin/env ruby
# bin/sidekiq (simplified)

require 'sidekiq/cli'
cli = Sidekiq::CLI.new
cli.parse  # Parse command line arguments
cli.run    # Start the main process

2. CLI Initialization Process

When Sidekiq::CLI.new is created, here’s what happens:

class Sidekiq::CLI
  def initialize
    # Set up signal handlers
    setup_signals

    # Parse configuration
    @config = Sidekiq::Config.new
  end

  def run
    # 1. Load Rails application
    load_application

    # 2. Setup Redis connection
    setup_redis

    # 3. Create the Manager (this is key!)
    @manager = Sidekiq::Manager.new(@config)

    # 4. Start the manager
    @manager.start

    # 5. Enter the main loop (THIS IS WHY IT DOESN'T EXIT!)
    wait_for_shutdown
  end
end

๐Ÿ”„ The Continuous Loop Architecture

Yes, it’s multiple loops! Here’s the hierarchy:

Main Process Loop

def wait_for_shutdown
  while !@done
    # Wait for shutdown signal (SIGTERM, SIGINT, etc.)
    sleep(SCAN_INTERVAL)

    # Check if we should gracefully shutdown
    check_shutdown_conditions
  end
end

Manager Loop

The Manager spawns and manages worker threads:

class Sidekiq::Manager
  def start
    # Spawn processor threads
    @workers = @concurrency.times.map do |i|
      Processor.new(self, &method(:processor_died))
    end

    # Start each processor thread
    @workers.each(&:start)

    # Start the poller thread (for scheduled jobs)
    @poller.start if @poller
  end
end

Processor Thread Loops (The Real Workers)

Each processor thread runs this loop:

class Sidekiq::Processor
  def run
    while !@done
      process_one_job
    end
  rescue Sidekiq::Shutdown
    # Graceful shutdown
  end

  private

  def process_one_job
    # 1. FETCH: Block and wait for a job from Redis
    job = fetch_job_from_redis  # This is where it "listens"

    # 2. PROCESS: Execute the job
    process_job(job) if job

    # 3. LOOP: Go back and wait for next job
  end
end

๐ŸŽง How It “Listens” for Jobs

The key is the Redis BRPOP command:

def fetch_job_from_redis
  # BRPOP = "Blocking Right Pop"
  # This blocks until a job is available!
  redis.brpop("queue:default", "queue:low", timeout: 2)
end

What BRPOP does:

  • Blocks the thread until a job appears in any of the specified queues
  • Times out after 2 seconds and checks again
  • Immediately returns when a new job is pushed to the queue

๐Ÿ” Step-by-Step Flow

Let’s trace what happens:

1. Startup
$ bin/sidekiq
# Creates CLI instance
# Loads Rails app
# Spawns 5 processor threads (default concurrency)
2. Each Thread Enters Listening Mode
# Thread 1, 2, 3, 4, 5 each run:
loop do
  job = redis.brpop("queue:default", timeout: 2)
  if job
    execute_job(job)
  end
  # Continue looping...
end
3. When You Queue a Job
# In your Rails app:
UserMailer.welcome_email(user).deliver_later

# This does:
redis.lpush("queue:default", job_data.to_json)
4. Immediate Response
  • One of the blocking BRPOP calls immediately receives the job
  • That thread processes the job
  • Goes back to listening for the next job

The process stays running because:

  1. Main thread sleeps and waits for shutdown signals
  2. Worker threads continuously loop, blocking on Redis
  3. No natural exit condition – it’s designed to run indefinitely
  4. Only exits when receiving termination signals (SIGTERM, SIGINT)

๐Ÿ“Š Visual Representation

Main Process
โ”œโ”€โ”€ Manager Thread
โ”œโ”€โ”€ Processor Thread 1 โ”€โ”€โ”
โ”œโ”€โ”€ Processor Thread 2 โ”€โ”€โ”ผโ”€โ”€โ”€ All blocking on redis.brpop()
โ”œโ”€โ”€ Processor Thread 3 โ”€โ”€โ”ผโ”€โ”€โ”€ Waiting for jobs...
โ”œโ”€โ”€ Processor Thread 4 โ”€โ”€โ”ผโ”€โ”€โ”€ Ready to process immediately
โ””โ”€โ”€ Processor Thread 5 โ”€โ”€โ”˜

Redis Queue: [job1, job2, job3] โ”€โ”€โ†’ BRPOP โ”€โ”€โ†’ Process job

1. ๐Ÿ›Œ What Does sleep Do in Ruby?

Yes, sleep pauses execution for the given number of seconds:

sleep(5)    # Pauses for 5 seconds
sleep(0.5)  # Pauses for 500 milliseconds
sleep(1.5)  # Pauses for 1.5 seconds
Why the while Loop is Needed

The code:

while !@done
  # Wait for shutdown signal (SIGTERM, SIGINT, etc.)
  sleep(SCAN_INTERVAL)
end

Without the loop, the process would:

sleep(SCAN_INTERVAL)  # Sleep once for ~2 seconds
# Then exit! ๐Ÿ˜ฑ

With the loop, it does this:

# Loop 1: Check if @done=false โ†’ sleep 2 seconds
# Loop 2: Check if @done=false โ†’ sleep 2 seconds  
# Loop 3: Check if @done=false โ†’ sleep 2 seconds
# ...continues forever until @done=true

Why This Pattern?

The main thread needs to:

  1. Stay alive to keep the process running
  2. Periodically check if someone sent a shutdown signal
  3. Not consume CPU while waiting
# Simplified version of what happens:
@done = false

# Signal handler (set up elsewhere)
Signal.trap("SIGTERM") { @done = true }

# Main loop
while !@done
  sleep(2)  # Sleep for 2 seconds
  # Wake up, check @done again
  # If @done=true, exit the loop and shutdown
end

puts "Shutting down gracefully..."

Real-world example:

$ bin/sidekiq
# Process starts, enters the while loop
# Sleeps for 2 seconds, checks @done=false, sleeps again...

# In another terminal:
$ kill -TERM <sidekiq_pid>
# This sets @done=true
# Next time the while loop wakes up, it sees @done=true and exits

2. ๐Ÿ”„ What is loop do in Ruby?

loop do is Ruby’s infinite loop construct:

loop do
  puts "This runs forever!"
  sleep(1)
end
Equivalent Forms

These are all the same:

# Method 1: loop do
loop do
  # code here
end

# Method 2: while true
while true
  # code here  
end

# Method 3: until false
until false
  # code here
end
Breaking Out of Loops
loop do
  puts "Enter 'quit' to exit:"
  input = gets.chomp

  break if input == "quit"  # This exits the loop

  puts "You said: #{input}"
end

puts "Goodbye!"
In Sidekiq Context
class Sidekiq::Processor
  def run
    loop do  # Infinite loop
      process_one_job

      # Only exits when:
      # 1. Exception is raised (like Sidekiq::Shutdown)
      # 2. break is called
      # 3. Process is terminated
    end
  rescue Sidekiq::Shutdown
    puts "Worker shutting down gracefully"
  end
end

๐Ÿ” The Difference in Context

Main Thread (with while and sleep):
# Purpose: Keep process alive, check for shutdown signals
while !@done
  sleep(2)  # "Lazy waiting" - check every 2 seconds
end
Worker Threads (with loop do):
# Purpose: Continuously process jobs without delay
loop do
  job = fetch_job  # This blocks until job available
  process(job)     # Process immediately
  # No sleep needed - fetch_job blocks for us
end
  1. sleep pauses for specified seconds – useful for “lazy polling”
  2. while !@done creates a “checkable” loop that can be stopped
  3. loop do creates an infinite loop for continuous processing
  4. Different purposes:
  • Main thread: “Stay alive and check occasionally”
  • Worker threads: “Process jobs continuously”

Simple analogy:

  • Main thread: Like a security guard who checks the building every 2 minutes
  • Worker threads: Like cashiers who wait for the next customer (blocking until one arrives)

๐Ÿ”’ How BRPOP Blocks Code

What “Blocking” Means

When we say BRPOP “blocks,” it means:

  • The thread stops executing and waits
  • No CPU is consumed during the wait
  • The thread is “parked” by the operating system
  • Execution resumes only when something happens

๐Ÿ” Step-by-Step: What Happens During BRPOP

1. The Call is Made
# Thread 1 executes this line:
job = redis.brpop("queue:default", "queue:low", timeout: 2)
2. Redis Connection Blocks
Ruby Thread 1 โ”€โ”€โ”€โ”€โ”
                  โ”‚
                  โ–ผ
Redis Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Redis Server
                      โ”‚
                      โ–ผ 
                   Check queues:
                   - queue:default โ†’ EMPTY
                   - queue:low โ†’ EMPTY

                   Result: WAIT/BLOCK
3. Thread Goes to Sleep
# At this point:
# - Thread 1 is BLOCKED (not consuming CPU)
# - Ruby interpreter parks this thread
# - Other threads continue running normally
# - The thread is "waiting" for Redis to respond
4. What Wakes Up the Block?

Option A: New Job Arrives

# Somewhere else in your Rails app:
SomeJob.perform_later(user_id)

# This does: redis.lpush("queue:default", job_data)
# โ†“
# Redis immediately responds to the waiting BRPOP
# โ†“ 
# Thread 1 wakes up with the job data
job = ["queue:default", job_json_data]

Option B: Timeout Reached

# After 2 seconds of waiting:
job = nil  # BRPOP returns nil due to timeout

๐Ÿงต Thread State Visualization

Before BRPOP:
Thread 1: [RUNNING] โ”€โ”€โ–บ Execute redis.brpop(...)

During BRPOP (queues empty):
Thread 1: [BLOCKED] โ”€โ”€โ–บ ๐Ÿ’ค Waiting for Redis response
Thread 2: [RUNNING] โ”€โ”€โ–บ Also calling redis.brpop(...)
Thread 3: [BLOCKED] โ”€โ”€โ–บ ๐Ÿ’ค Also waiting
Thread 4: [RUNNING] โ”€โ”€โ–บ Processing a job
Thread 5: [BLOCKED] โ”€โ”€โ–บ ๐Ÿ’ค Also waiting

Job arrives via LPUSH:
Thread 1: [RUNNING] โ”€โ”€โ–บ Wakes up! Got the job!
Thread 2: [BLOCKED] โ”€โ”€โ–บ Still waiting
Thread 3: [BLOCKED] โ”€โ”€โ–บ Still waiting  

โšก Why This is Efficient

Blocking vs Polling Comparison

โŒ Bad Approach (Polling):

loop do
  job = redis.rpop("queue:default")  # Non-blocking
  if job
    process(job)
  else
    sleep(0.1)  # Check again in 100ms
  end
end

# Problems:
# - Wastes CPU checking every 100ms
# - Delays job processing by up to 100ms
# - Not scalable with many workers

โœ… Good Approach (BRPOP Blocking):

loop do
  job = redis.brpop("queue:default", timeout: 2)  # Blocking
  process(job) if job
end

# Benefits:
# - Zero CPU usage while waiting
# - Instant job processing (no polling delay)
# - Scales to thousands of workers

๐Ÿ› ๏ธ System-Level Explanation

What Happens in the OS
  1. Ruby calls Redis client
  2. Redis client opens TCP socket to Redis server
  3. Sends BRPOP command over socket
  4. Thread calls system sleep() – goes into “waiting” state
  5. OS scheduler removes thread from active CPU queue
  6. Thread doesn’t run until socket receives data
Ruby Process
โ”œโ”€โ”€ Thread 1 [BLOCKED on socket read]
โ”œโ”€โ”€ Thread 2 [RUNNING - processing job]  
โ”œโ”€โ”€ Thread 3 [BLOCKED on socket read]
โ””โ”€โ”€ Thread 4 [BLOCKED on socket read]

Operating System Scheduler:
- Only schedules Thread 2 for CPU time
- Threads 1,3,4 are "sleeping" - zero CPU usage

๐Ÿ“ก Network Level Detail

Client                     Redis Server
  โ”‚                           โ”‚
  โ”‚โ”€โ”€โ”€โ”€โ–บ BRPOP queue:default โ”€โ”ค
  โ”‚                           โ”‚ Check queue...
  โ”‚                           โ”‚ Empty!
  โ”‚                           โ”‚ Add client to 
  โ”‚                           โ”‚ waiting list
  โ”‚                           โ”‚
  โ”‚ ๐Ÿ’ค BLOCKED               โ”‚
  โ”‚                           โ”‚
  โ”‚                           โ”‚ โ—„โ”€โ”€โ”€โ”€ LPUSH from Rails app
  โ”‚                           โ”‚ 
  โ”‚ โ—„โ”€โ”€โ”€โ”€ job_data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค Wake up waiting client!
  โ”‚                           โ”‚
  โ”‚ Thread RESUMES           โ”‚
  1. BRPOP literally pauses thread execution at the OS level
  2. Zero CPU consumption during blocking
  3. Instant response when job arrives (no polling delay)
  4. Multiple threads can block simultaneously on different/same queues
  5. Redis manages the waiting list of blocked clients
  6. Timeout prevents infinite blocking if no jobs arrive

This is why Sidekiq is so efficient – workers spend most of their time blocked (consuming zero resources) and instantly wake up when work arrives!

The blocking mechanism is what makes the “always listening” behavior possible without eating up your server’s resources.


How BRPOP is actually implemented? Sidekiq doesn’t implement it directly – it uses the redis gem, which is the standard Redis client for Ruby.

๐Ÿš€ How redis.brpop is Implemented in Ruby

The Reality: It’s Actually Simple

The Redis gem doesn’t implement BRPOP itself – it delegates to a lower-level client that handles the actual socket communication. Here’s the architecture:

๐Ÿ—๏ธ The Ruby Implementation Stack

1. High-Level Redis Gem
# In your Sidekiq code
redis.brpop("queue:default", "queue:low", timeout: 2)

2. Redis Gem Delegation

The Redis gem (the one Sidekiq uses) primarily does:

# Simplified version in the Redis gem
def brpop(*keys, **options)
  timeout = options[:timeout] || 0

  # Convert arguments to Redis protocol format
  command = ["BRPOP"] + keys + [timeout]

  # Delegate to lower-level client
  call(command)
end

3. Lower-Level Client (redis-client)

The actual networking happens in the redis-client gem:

# In redis-client gem
class RedisClient
  def call(command, timeout: nil)
    # 1. Format command according to RESP protocol
    command_string = format_command(command)

    # 2. Send to Redis server
    @socket.write(command_string)

    # 3. READ AND BLOCK HERE! 
    # This is where the blocking magic happens
    response = @socket.read_with_timeout(timeout)

    # 4. Parse and return response
    parse_response(response)
  end
end

๐Ÿ” The Critical Blocking Part

Here’s where the blocking actually happens:

# Simplified socket read implementation
def read_with_timeout(timeout)
  if timeout && timeout > 0
    # Use IO.select to wait for data with timeout
    ready = IO.select([@socket], nil, nil, timeout)

    if ready
      # Data is available, read it
      @socket.read_nonblock(4096)
    else
      # Timeout reached, return nil
      nil
    end
  else
    # Block indefinitely until data arrives
    @socket.read  # THIS BLOCKS THE THREAD
  end
end

โšก The Socket-Level Magic

At the lowest level, it’s just Ruby’s socket operations:

# What actually happens at the OS level
require 'socket'

socket = TCPSocket.new('localhost', 6379)

# Send BRPOP command
socket.write("*4\r\n$5\r\nBRPOP\r\n$13\r\nqueue:default\r\n$9\r\nqueue:low\r\n$1\r\n2\r\n")

# THIS BLOCKS until Redis responds or timeout
result = socket.read  # Ruby thread blocks here

socket.close

๐Ÿงต Ruby’s Built-in Blocking

The secret sauce: Ruby’s socket.read method is built-in blocking:

  1. Ruby calls the OS: socket.read translates to system calls like recv() or read()
  2. OS blocks the thread: The operating system puts the thread to sleep
  3. Zero CPU usage: Thread consumes no resources while waiting
  4. Instant wake-up: OS wakes thread when data arrives

๐ŸŽฏ Real Implementation Example

Here’s what a simplified BRPOP implementation looks like:

class SimpleRedisClient
  def initialize(host, port)
    @socket = TCPSocket.new(host, port)
  end

  def brpop(key, timeout = 0)
    # Format Redis command
    command = "*3\r\n$5\r\nBRPOP\r\n$#{key.length}\r\n#{key}\r\n$#{timeout.to_s.length}\r\n#{timeout}\r\n"

    # Send command
    @socket.write(command)

    # BLOCK and wait for response
    # This is where the magic happens!
    response = @socket.read

    # Parse response
    parse_redis_response(response)
  end

  private

  def parse_redis_response(response)
    # Parse Redis RESP protocol
    # Return parsed data or nil for timeout
  end
end
  1. No Ruby “magic” – just standard socket operations
  2. OS handles the blocking – not Ruby-specific code
  3. Thread sleeps at kernel level – zero CPU usage
  4. IO.select for timeouts – Ruby’s standard approach
  5. RESP protocol – Redis’s simple text protocol

The “blocking” is just Ruby’s normal socket behaviour – when you read from a socket with no data, the thread naturally blocks until data arrives!

This is why BRPOP is so efficient – it leverages the operating system’s built-in ability to efficiently wait for network data without consuming any CPU resources.

Pretty elegant, right? The complexity is all hidden in the OS networking stack, while the Ruby implementation stays remarkably simple! ๐ŸŽ‰


Unknown's avatar

Author: Abhilash

Hi, Iโ€™m Abhilash! A seasoned web developer with 15 years of experience specializing in Ruby and Ruby on Rails. Since 2010, Iโ€™ve built scalable, robust web applications and worked with frameworks like Angular, Sinatra, Laravel, Node.js, Vue and React. Passionate about clean, maintainable code and continuous learning, I share insights, tutorials, and experiences here. Letโ€™s explore the ever-evolving world of web development together!

Leave a comment