Category: Software

WaterDrop Meets Ruby’s Async Ecosystem: Lightweight Concurrency Done Right

Ruby developers have faced an uncomfortable truth for years: when you need to talk to external systems like Kafka, you're going to block. Sure, you could reach for heavyweight solutions like EventMachine, Celluloid, or spawn additional threads, but each comes with its own complexity tax.

EventMachine forces you into callback hell. Threading introduces race conditions and memory overhead. Meanwhile, other ecosystems had elegant solutions: Go's goroutines, Node.js's event loops, and Python's asyncio.

Ruby felt clunky for high-performance I/O-bound applications.

Enter the Async Gem

Samuel Williams' async gem brought something revolutionary to Ruby: lightweight concurrency that actually feels like Ruby. No callbacks. No complex threading primitives. Just fibers.

require 'async'

Async do |task|
  # These run concurrently
  task1 = task.async { fetch_user_data }
  task2 = task.async { fetch_order_data }
  task3 = task.async { fetch_metrics_data }

  [task1, task2, task3].each(&:wait)
end

The genius is in the underlying architecture. When an I/O operation would normally block, the fiber automatically yields control to other fibers – no manual coordination is required.

Why Lightweight Concurrency Matters

Traditional threading and evented architectures are heavy. Threads consume a significant amount of memory (1MB stack per thread) and come with complex synchronization requirements. Event loops force you to restructure your entire programming model.

Fibers are lightweight:

  • Memory efficient: Kilobytes instead of megabytes
  • No synchronization complexity: Cooperative scheduling
  • Familiar programming model: Looks like regular Ruby code
  • Automatic yielding: Runtime handles I/O coordination

WaterDrop: Built for Async

Starting with the 2.8.7 release, every #produce_sync and #produce_many_sync operation in WaterDrop automatically yields during Kafka I/O. You don't configure it. It just works:

require 'async'
require 'waterdrop'

producer = WaterDrop::Producer.new do |config|
  config.kafka = { 'bootstrap.servers': 'localhost:9092' }
end

Async do |task|
  # These run truly concurrently
  user_events = task.async do
    100.times do |i|
      producer.produce_sync(
        topic: 'user_events',
        payload: { user_id: i, action: 'login' }.to_json
      )
    end
  end

  # This also runs concurrently during Kafka I/O
  metrics_task = task.async do
    collect_application_metrics
  end

  [user_events, metrics_task].each(&:wait)
end

Real Performance Impact

Performance Note: These benchmarks show single-message synchronous production (produce_sync) for clarity. WaterDrop also supports batch production (produce_many_sync), async dispatching (produce_async), and promise-based workflows. When combined with fibers, these methods can achieve much higher throughput than shown here.

I benchmarked a Rails application processing 10,000 Kafka messages across various concurrency patterns:

Sequential processing (baseline):

  • Total time: 62.7 seconds
  • Throughput: 160 messages/second
  • Memory overhead: Baseline

Single fiber (no concurrency):

  • Total time: 63.2 seconds
  • Throughput: 158 messages/second
  • Improvement: 0.99x - No benefit without actual concurrency

Real-world scenario (3 concurrent event streams):

  • Total time: 23.8 seconds
  • Throughput: 420 messages/second
  • Improvement: 2.6x - What most applications will see in production

Optimized fiber concurrency (controlled batching):

  • Total time: 12.6 seconds
  • Throughput: 796 messages/second
  • Improvement: 5.0x - Peak performance with proper structure

Multiple producers (traditional parallelism):

  • Total time: 15.2 seconds
  • Throughput: 659 messages/second
  • Improvement: 4.1x - Good, but uses more memory than fibers

A single producer using fibers outperforms multiple producer instances (5.0x vs 4.1x) while using less memory and resources. This isn't about making individual operations faster - it's about enabling Ruby to handle concurrent I/O elegantly and efficiently.

Transparent Integration

What makes WaterDrop's async integration cool is that it's completely transparent:

# This code works with or without async
producer.produce_sync(
  topic: 'events',
  payload: data.to_json
)

Running in a fiber scheduler? It yields during I/O. Running traditionally? It blocks normally. No configuration. No special methods.

The Transactional Reality

Transactions have limitations. Multiple transactions from one producer remain sequential due to the transactional.id design:

# These transactions will block each other
Async do |task|
  task.async { producer.transaction { ... } }
  task.async { producer.transaction { ... } } # Waits for first
end

But: transactions still yield during I/O, allowing other fibers doing different work to continue. For concurrent transactions, use separate producers.

Real-World Example

class EventProcessor
  def process_user_activity(sessions)
    Async do |task|
      # Process different types concurrently
      login_task = task.async { process_logins(sessions) }
      activity_task = task.async { process_activity(sessions) }

      # Analytics runs during Kafka I/O
      analytics_task = task.async { update_analytics(sessions) }

      [login_task, activity_task, analytics_task].each(&:wait)
    end
  end

  private

  def process_logins(sessions)
    sessions.each do |session|
      producer.produce_sync(
        topic: 'user_logins',
        payload: session.to_json
      )
    end
  end
end

Why This Matters

WaterDrop's async integration proves Ruby can compete in high-performance I/O scenarios without sacrificing elegance. Combined with Samuel's broader ecosystem (async-http, async-postgres, falcon), you get a complete stack for building high-performance Ruby applications.

Try wrapping any I/O-heavy operations in Async do |task| blocks. Whether it's API calls, database queries, or Kafka operations with WaterDrop, the performance improvement may be immediate and dramatic.


Find WaterDrop on GitHub and explore the async ecosystem that's making Ruby fast again.

Announcing Passive Queue: The Rails Background Job System That Transcends Processing

0ms processing time. Infinite scalability. 100% success rate. Zero failures.

GitHub stars ← Give it a star if you enjoyed this zen approach!

The Moment of Zen Clarity

It was Day Two of RailsConf 2025 in sweltering Philadelphia when the idea crystallized. My friend Justin and I were discussing the endless cycle of Rails optimization - everyone building faster queues, better job processors, more efficient background systems.

That's when Justin sparked the initial concept: What if we built a "Passive Job" library?

The idea was brilliant, but as I thought it through, I realized it should align with the existing ecosystem. We weren't replacing ActiveJob itself - we were creating a queue backend.

The Problem We Didn't Know We Had

Modern Rails development has become an endless cycle of optimization. We have Solid Queue, Sidekiq, Karafka, Good Job and many other. Each one promises to process your background jobs faster, more efficiently, more reliably.

But what if the real problem isn't that our jobs are too slow? What if the problem is that they run at all?

Introducing Passive Queue

Today, I'm excited to announce Passive Queue - a Rails queue adapter embracing the zen of non-execution. It's the queue backend for developers who understand that the best job is the one never done.

Features That Don't Exist

  • 100% reliable non-execution - Your jobs will never fail because they never run
  • Infinite scalability - Nothing scales better than nothing
  • Zero memory footprint - Truly efficient resource usage
  • 0ms processing time - Unbeatable performance metrics
  • Perfect success rate - At doing absolutely nothing

One-Line Installation

# In your Rails application.rb
config.active_job.queue_adapter = :passive_queue

That's it. Every single ActiveJob in your Rails application will now be processed with the serene calm of non-execution.

The Philosophy Behind the Madness

Passive Queue isn't just a parody-it's a meditation on our industry's obsession with doing more, faster, all the time. Sometimes the most zen approach is to simply accept that not everything needs to be done.

Think about it:

  • Newsletter emails that were never sent can't end up in spam folders
  • Data processing jobs that never run can't corrupt your database
  • Image resizing tasks that don't execute can't fill up your disk space

It's not a bug, it's enlightenment.

The Command Line Experience

But Passive Queue isn't just about background jobs. It comes with a meditation tool for developers:

bundle exec be passive                   # Basic meditation
bundle exec be passive --zen             # With zen quotes
bundle exec be passive --philosophical   # Deep thoughts
bundle exec be passive --aggressive      # Don't do this

Each command helps you embrace the art of doing nothing, perfect for those moments between deployments when you need to center yourself.

The Dashboard of Enlightenment

For teams that need visual confirmation of their non-productivity, Passive Queue includes a beautiful web dashboard. Mount it as a Rack engine in your Rails app:

# config/routes.rb
Rails.application.routes.draw do
  mount PassiveQueue::Engine => "/passive_queue"
end

Visit /passive_queue and you'll be greeted with a stunning dashboard featuring:

  • Real-time job counts (always zero, beautifully displayed)
  • Processing queues (eternally empty, perfectly styled)
  • Performance metrics (infinite efficiency charts)
  • Worker status (serenely inactive indicators)
  • Zen quotes (rotating wisdom for your enlightenment)

The dashboard is fully responsive, beautifully designed, and does absolutely nothing functional. It's the perfect way to demonstrate your commitment to the zen of non-execution to stakeholders who need to see data to feel comfortable.

Born at RailsConf 2025

This project was born during those sweaty Philadelphia days, in conversations with my friend Justin about the state of Rails tooling. Sometimes the best ideas come from collaborative moments where you're questioning everything we take for granted in web development.

During the panel on background processing with Mike Rosa and Ben, we discussed the evolution of queueing systems in Ruby. But it was Justin's and my original "Passive Job" concept that made me realize we needed to take this further-Passive Queue represents the next logical step: transcendence through non-action.

The Technical Implementation

Under the hood, Passive Queue is surprisingly sophisticated in its simplicity.

It implements the full ActiveJob adapter interface, ensuring compatibility with your existing Rails application. The difference is that every job returns successfully_not_processed instead of actually running.

Zero Dependencies, Maximum Zen

True to its philosophy, Passive Queue has zero runtime dependencies. It doesn't rely on Redis, PostgreSQL, or even ActiveJob (though it integrates beautifully when available). Pure Ruby, pure zen.

Get Started Today

Install the gem:

gem install passive_queue

Add it to your Rails app:

config.active_job.queue_adapter = :passive_queue

And experience the tranquility of knowing your background jobs are in a state of perfect, permanent rest.

The Future of Non-Productivity

This is just the beginning. The dashboard is already inspiring ideas for PassiveRecord (an ORM that never queries), PassiveCache (a cache that never stores), and maybe even PassiveRails (a web framework that serves only 204 No Content responses).

But for now, Passive Queue stands alone as a testament to the power of doing nothing, doing it well, and making it look absolutely gorgeous while doing so.

Join the Movement

You can find Passive Queue on GitHub and RubyGems. Contributions are welcome, though we ask that you contribute in the spirit of the project-which is to say, perhaps consider not contributing at all.

The gem is MIT licensed, because even non-productivity should be free.


Sometimes the most profound action is inaction. Sometimes the best code is no code. And sometimes, the perfect queue is the one that queues forever.

Try Passive Queue today. Your background jobs will thank you by never running.

Copyright © 2025 Closer to Code

Theme by Anders NorenUp ↑