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
- CLI Initialization: Sidekiq starts via
bin/sidekiq, which creates aSidekiq::CLIinstance - Configuration Loading: Parses YAML config files and command-line arguments
- Application Loading: Requires your Rails application or specified Ruby files
- 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
- Job Enqueueing: Jobs are pushed to Redis lists using
LPUSH - Job Fetching: Worker processes use
BRPOPto atomically fetch jobs - Execution: Each job runs in its own thread within a processor
- 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:
- Job Storage: All job data is stored in Redis lists and sorted sets
- Queue Management: Different queues are implemented as separate Redis lists
- Scheduling: Future and retry jobs use Redis sorted sets with timestamps
- 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):
- Stops accepting new jobs
- Allows current jobs to complete (with timeout)
- Requeues any unfinished jobs back to Redis
- 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:
- Different use cases: Not every app needs Redis-level performance
- Rails philosophy: “Convention over configuration” includes sensible defaults
- Accessibility: Lower barrier to entry for new Rails developers
- 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
BRPOPcalls immediately receives the job - That thread processes the job
- Goes back to listening for the next job
The process stays running because:
- Main thread sleeps and waits for shutdown signals
- Worker threads continuously loop, blocking on Redis
- No natural exit condition – it’s designed to run indefinitely
- 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:
- Stay alive to keep the process running
- Periodically check if someone sent a shutdown signal
- 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
sleeppauses for specified seconds – useful for “lazy polling”while !@donecreates a “checkable” loop that can be stoppedloop docreates an infinite loop for continuous processing- 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
- Ruby calls Redis client
- Redis client opens TCP socket to Redis server
- Sends BRPOP command over socket
- Thread calls system sleep() – goes into “waiting” state
- OS scheduler removes thread from active CPU queue
- 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 โ
- BRPOP literally pauses thread execution at the OS level
- Zero CPU consumption during blocking
- Instant response when job arrives (no polling delay)
- Multiple threads can block simultaneously on different/same queues
- Redis manages the waiting list of blocked clients
- 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:
- Ruby calls the OS:
socket.readtranslates to system calls likerecv()orread() - OS blocks the thread: The operating system puts the thread to sleep
- Zero CPU usage: Thread consumes no resources while waiting
- 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
- No Ruby “magic” – just standard socket operations
- OS handles the blocking – not Ruby-specific code
- Thread sleeps at kernel level – zero CPU usage
- IO.select for timeouts – Ruby’s standard approach
- 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! ๐