<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged sidekiq</title>
<link>https://coderlegion.com/tag/sidekiq</link>
<description>Powered by Question2Answer</description>
<item>
<title>Better Sidekiq Classes</title>
<link>https://coderlegion.com/5088/better-sidekiq-classes</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sidekiq/sidekiq&quot;&gt;Sidekiq&lt;/a&gt; is pretty much the go-to solution for enqueuing jobs for background processing when working on a Ruby-based project. It's simple to implement, has a clear DSL, and is well-supported by common testing frameworks like RSpec. That being said, and I may be nitpicking, but the standard implementation examples do add a little extra overhead if you wish to be more flexible with your job business logic, and additional test setup is required.&lt;/p&gt;
&lt;h3 id=&quot;the-normal-way&quot;&gt;The &quot;normal&quot; way&lt;/h3&gt;
&lt;p&gt;Given we want a little background job that updates a user's name and any other business logic needed (to keep the example simple), we could create a standard Sidekiq job with that logic and call it from our code as such:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class UpdateUserJob
  include Sidekiq::Job

  def perform(user_id, name)
    user = User.find(user_id)
    user.update!(name: name)
    # .. additional business logic here
  end
end

UpdateUserJob.perform_async(1, &quot;bob&quot;) # perform the job as soon as possible
UpdateUserJob.perform_in(5.minutes, 1, &quot;bob&quot;) # delay the processing by 5 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is simple enough. But what if we want to test the logic being performed inside this job? Firstly, we'd need to update our &lt;code&gt;spec_helper&lt;/code&gt; to let it know what to do with a job. If you're using RSpec, you might even need to add the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/philostler/rspec-sidekiq&quot;&gt;rspec-sidekiq&lt;/a&gt; gem for additional configuration and matcher options. A classic-looking spec for this job might look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;it &quot;updates the user&quot; do
  UpdateUserJob.perform_async(1, &quot;bob&quot;)
  expect(UpdateUserJob.jobs.size).to eq(1)
  UpdateUserJob.drain
  expect(User.find(1).name).to eq(&quot;bob&quot;)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to &lt;code&gt;drain&lt;/code&gt; the job before we can test the result of its work. Alternatively, we could wrap the test inside one of the Sidekiq testing helper methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;it &quot;updates the user&quot; do
  Sidekiq::Testing.inline! do
    UpdateUserJob.perform_async(1, &quot;bob&quot;)
    expect(User.find(1).name).to eq(&quot;bob&quot;)
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This does work as expected however it does leave a little to be desired. We have to remember to do this whenever testing job logic (the number of times I've had to search through previous specs to remind myself of the syntax), it adds a few extra lines to the tests which are not necessary and there is a spec performance hit as well.&lt;/p&gt;
&lt;h3 id=&quot;the-use-case-way&quot;&gt;The &quot;use case&quot; way&lt;/h3&gt;
&lt;p&gt;So, what options do we have? Well, the first option would be to extract the logic performed by the job into a class which we would then pass the job attributes to for processing. That way we can independently test the business logic without having to mess with background workers and the like. I have used various names for these classes, &lt;strong&gt;Use Case&lt;/strong&gt;, &lt;strong&gt;Resource&lt;/strong&gt;, or &lt;strong&gt;Library&lt;/strong&gt; to name a few (use whatever best suits you and your conventions). Our new job would look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class UpdateUserJob
  include Sidekiq::Job

  def perform(user_id, name)
    UpdateUserUseCase.new(user_id, name).call
  end
end

class UpdateUserUseCase
  def initialize(user_id, name)
    @user_id = user_id
    @name = name
  end

  def call
    user = User.find(@user_id)
    user.update!(name: @name)
    # .. additional business logic here
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, when it comes to testing, we could just forgo testing the actual job class as it does nothing except pass on some arguments to a plain old Ruby object. We can just test the class directly. Sidekiq is also already extensively tested so there is little point in testing whether the jobs on the queue have increased or not unless you are expecting new jobs to be created as a knock-on effect of your business logic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;it &quot;updates the user&quot; do
  UpdateUserUseCase.new(1, &quot;bob&quot;).call
  expect(User.find(1).name).to eq(&quot;bob&quot;)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The added benefit to doing it this way is that you have a plain old Ruby object that you can call independently and inline in your code elsewhere should the need arise. For instance, you may want to defer the processing to the background when handling a mass import of data, however, when being performed as a single HTTP request to your API, you could wish to do it inline.&lt;/p&gt;
&lt;p&gt;The downside to this is that we now, however, have had to create an additional class to provide this flexibility. That means, if we wish to keep this convention going, for each &lt;code&gt;Job&lt;/code&gt; class, there would be a corresponding &lt;code&gt;UseCase&lt;/code&gt; class. What if we could take this idea one step further? What if we could turn our Job class into a plain old Ruby object right from the start? Let's rewrite our &lt;code&gt;UpdateUserJob&lt;/code&gt; class.&lt;/p&gt;
&lt;h3 id=&quot;the-runner-way-with-poros-&quot;&gt;The &quot;runner&quot; way (with POROs)&lt;/h3&gt;
&lt;p&gt;Let's pretend you had never heard of Sidekiq or background queues, and you want to write some code to perform a specific task. For the sake of convention, I have ended the following class name with &quot;Job&quot; to match previous examples however by syntax definition, it is no longer what we would call a classic &quot;job&quot;. It is now just a Plain Old Ruby Object (PORO; identical to our previous &lt;code&gt;UpdateUserUseCase&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class UpdateUserJob
  def initialize(user_id, name)
    @user_id = user_id
    @name = name
  end

  def call
    user = User.find(@user_id)
    user.update!(name: @name)
    # .. additional business logic here
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that in place, we need to set up some sort of mechanism to have this code be run by Sidekiq in our background queue. We can introduce a simple class to wrap the sidekiq calls, allowing us to inject our custom job class with its arguments. To not conflict with the Sidekiq namespace, I have created the &lt;code&gt;Runners&lt;/code&gt; module which will run this code for me &lt;em&gt;(side note: no matter what project I've worked on in the past, I always seem to use these &quot;runner&quot; style classes for various things)&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt; # Cannot figure out why this code block fails to format correctly here on CoderLegion
module Runners
  class Sidekiq
    include ::Sidekiq::Job

    sidekiq_options(retry: 3, backtrace: true, queue: &quot;default&quot;)

    def perform(job_class_name, *args)
      Kernel.const_get(job_class_name).new(*args).call
    end

    def run(job_class, *args)
      self.class.perform_async(job_class.to_s, *args)
    end

    def run_in(delay_in_seconds, job_class, *args)
      self.class.perform_in(delay_in_seconds, job_class.to_s, *args)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, we have everything we need to be able to throw plain old Ruby classes into the background for processing. If we wish to run our business logic inline, we simply run&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;UpdateUserJob.new(1, &quot;bob&quot;).call
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but if we want to delegate the processing to the background queue, we can send it off immediately or delay it&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Runners::Sidekiq.new.run(UpdateUserJob, 1, &quot;bob&quot;) # perform the job as soon as possible
Runners::Sidekiq.new.run_in(5.minutes, UpdateUserJob, 1, &quot;bob&quot;) # delay the processing by 5 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://www.brazenbraden.com/assets/images/sidekiq_classes/update_user_job.png&quot; alt=&quot;Sidekiq Web View&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;bells-amp-whistles&quot;&gt;Bells &amp;amp; Whistles&lt;/h3&gt;
&lt;p&gt;Now that we have our lovely Sidekiq runner class, we can embellish it with a couple final bells and whistles. We might want to add / override the sidekiq_options on demand instead of being stuck with the previous hard-coded options. Perhaps we prefer using keyword arguments in our method definitions for the added clarity and ability to insert the arguments in any order. And maybe, we would like the job names in the Sidekiq UI to be a little more descriptive as to the actual job they are running so that we don't have to go digging through the arguments list to find specific jobs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt; # Cannot figure out why this code block fails to format correctly here on CoderLegion
module Runners
  class Sidekiq
    include ::Sidekiq::Job

    DEFAULT_OPTIONS = { retry: 3, backtrace: true, queue: &quot;default&quot; }

    def perform(job_class_name, *args)
      if args in [Hash]
        # This handles the case of keyword arguments
        job_class_name.constantize.new(**args.first.symbolize_keys!).call
      else
        # This handles the case of basic arguments
        job_class_name.constantize.new(*args).call
      end
    end

    def initialize(options = {})
      self.class.sidekiq_options(DEFAULT_OPTIONS.merge(options))
    end

    def run(job_class, *args)
      update_display_class(job_class)
      self.class.perform_async(job_class.to_s, *args)
    end

    def run_in(delay_in_seconds, job_class, *args)
      update_display_class(job_class)
      self.class.perform_in(delay_in_seconds, job_class.to_s, *args)
    end

    private

    def update_display_class(job_class)
      self.class.sidekiq_options.merge!(&quot;display_class&quot; =&amp;gt; &quot;#{job_class} [#{self.class.name}]&quot;)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In essence, you can now define your background job classes as basic Ruby objects, execute them inline, or transfer them to Sidekiq for background processing via our Runner. The main requirement is maintaining a consistent DSL with your job classes, such as implementing a mandatory &lt;code&gt;call&lt;/code&gt; (or equivalent: &lt;code&gt;perform&lt;/code&gt; or &lt;code&gt;execute&lt;/code&gt;) method. (Kudos to my friend &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/krzyczak&quot;&gt;Kris&lt;/a&gt; for the original idea.)&lt;/p&gt;
</description>
<category>Ruby on Rails</category>
<guid isPermaLink="true">https://coderlegion.com/5088/better-sidekiq-classes</guid>
<pubDate>Thu, 28 Aug 2025 23:31:09 +0000</pubDate>
</item>
<item>
<title>Scaling Sidekiq in Ruby on Rails: Best Practices, War Stories, and WTF Moments</title>
<link>https://coderlegion.com/4983/scaling-sidekiq-in-ruby-on-rails-best-practices-war-stories-and-wtf-moments</link>
<description>&lt;p&gt;Imagine that your Rails application is running smoothly. Users are satisfied, payments are processed, emails are sent out, and reports are produced. Sidekiq feels like magic.&lt;/p&gt;
&lt;p&gt;Then one Friday evening (because production issues always happen on Fridays), you check the Sidekiq dashboard ????&lt;/p&gt;
&lt;p&gt;Jobs are piling up faster than your interns can say, “Did we forget to add sidekiq_options retry: false again?”&lt;/p&gt;
&lt;p&gt;Welcome to Sidekiq at scale—where background jobs stop being background and start being your entire life.&lt;/p&gt;
&lt;p&gt;Here are some lessons I've learned from implementing Sidekiq at scale in production, along with metrics and real-world examples.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-keep-jobs-small-and-fast&quot;&gt;1. Keep Jobs Small and Fast&lt;/h3&gt;
&lt;p&gt;Jobs should be atomic and quick. A &lt;strong&gt;5–10 second job is already too long&lt;/strong&gt; in most production systems.&lt;/p&gt;
&lt;p&gt;Bad example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class ReportJob
  def perform(user_id)
    user = User.find(user_id)
    generate_pdf(user)          # CPU-heavy
    upload_to_s3(user)          # I/O-heavy
    send_email(user)            # External API
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this fails halfway, the entire job retries.&lt;/p&gt;
&lt;p&gt;✅ Better:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class GeneratePdfJob &amp;lt; ApplicationJob
  def perform(user_id)
    PdfGenerator.call(User.find(user_id))
  end
end

class UploadReportJob &amp;lt; ApplicationJob
  def perform(file_path)
    S3Uploader.call(file_path)
  end
end

class SendReportJob &amp;lt; ApplicationJob
  def perform(user_id, report_id)
    ReportMailer.with(user_id:, report_id:).deliver_now
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chain jobs with callbacks or Sidekiq batches.&lt;/p&gt;
&lt;h3 id=&quot;2-idempotency-is-non-negotiable&quot;&gt;2. Idempotency is Non-Negotiable&lt;/h3&gt;
&lt;p&gt;Jobs will run &lt;strong&gt;at least once&lt;/strong&gt;, sometimes more. If jobs aren’t idempotent, you’ll double-charge customers or send duplicate emails.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;order.mark_as_paid!
send_receipt(order)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;order.update!(status: &quot;paid&quot;) unless order.paid?
ReceiptMailer.send(order).deliver_later unless order.receipt_sent?
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-use-queues-strategically&quot;&gt;3. Use Queues Strategically&lt;/h3&gt;
&lt;p&gt;Don’t dump everything into &lt;code&gt;default&lt;/code&gt;. Separate workloads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;critical&lt;/code&gt; → user-facing (emails, webhooks, payments)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default&lt;/code&gt; → normal jobs (notifications, syncs)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;low&lt;/code&gt; → heavy, non-urgent (reports, exports, ETL)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;sidekiq.yml&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;:queues:
  - [critical, 5]
  - [default, 3]
  - [low, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-monitor-everything&quot;&gt;4. Monitor Everything&lt;/h3&gt;
&lt;p&gt;Visibility is critical at scale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sidekiq Web UI&lt;/strong&gt; for queue depth &amp;amp; retries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SQL snippet to monitor retries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT count(*)
FROM sidekiq_jobs
WHERE queue = 'default'
AND retry = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-control-concurrency&quot;&gt;5. Control Concurrency&lt;/h3&gt;
&lt;p&gt;More threads ≠ better throughput. High concurrency can overwhelm Postgres or external APIs.&lt;/p&gt;
&lt;p&gt;✅ Start with concurrency = &lt;strong&gt;2–5x your CPU cores&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;:concurrency: 15
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-redis-is-your-heartbeat&quot;&gt;6. Redis is Your Heartbeat&lt;/h3&gt;
&lt;p&gt;Redis is the single point of truth for Sidekiq.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;dedicated Redis&lt;/strong&gt; (not shared with cache/session store)&lt;/li&gt;
&lt;li&gt;Monitor memory &amp;amp; latency&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-smart-retries-only&quot;&gt;7. Smart Retries Only&lt;/h3&gt;
&lt;p&gt;Default retries (up to 25) can flood your system. Customize them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class MyJob
  sidekiq_options retry: 3

  def perform
    ExternalService.call!
  rescue ExternalService::InvalidCredentials
    # Don’t retry, just fail fast
    raise Sidekiq::Shutdown
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-deploy-with-process-separation&quot;&gt;8. Deploy with Process Separation&lt;/h3&gt;
&lt;p&gt;Don’t run one mega Sidekiq instance. Separate by queue type.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bundle exec sidekiq -q critical,5 -q default,2
bundle exec sidekiq -q low
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-dead-jobs-are-zombie-failures&quot;&gt;9. Dead Jobs are Zombie Failures&lt;/h3&gt;
&lt;p&gt;Dead jobs = silent failures.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bundle exec sidekiqctl clean --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or query them in Redis:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;Sidekiq::DeadSet.new.size
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-autoscale-or-suffer&quot;&gt;10. Autoscale or Suffer&lt;/h3&gt;
&lt;p&gt;In Kubernetes, ECS, or Heroku, autoscale workers on queue latency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scale up if latency &amp;gt; 30s&lt;/li&gt;
&lt;li&gt;Scale down if latency &amp;lt; 5s&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;11-chain-jobs-for-external-api-pagination&quot;&gt;11. Chain Jobs for External API Pagination&lt;/h3&gt;
&lt;p&gt;Sometimes, the firehose comes from outside your app. A classic case: syncing a huge dataset from a third-party API that only gives you paginated results.&lt;/p&gt;
&lt;p&gt;If you naively fetch all pages in one job, you’ll hit rate limits, blow up memory, and make retries a nightmare.&lt;/p&gt;
&lt;p&gt;✅ Better: chain jobs page by page. Each job handles one page, then enqueues the next, until the API says you’re done. That way:&lt;br&gt;
Jobs stay tiny and idempotent.&lt;/p&gt;
&lt;p&gt;Failures retry gracefully without restarting the whole sync.&lt;br&gt;
You respect API rate limits with backoff and jitter.&lt;/p&gt;
&lt;p&gt;You can fan out each page’s items to dedicated jobs for massive throughput.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class SyncExternalPage
  include Sidekiq::Worker
  sidekiq_options queue: :sync, retry: 10

  def perform(cursor = nil)
    response = ExternalClient.fetch_page(cursor: cursor)

    response[:items].each do |raw|
      UpsertExternalItem.perform_async(raw) # idempotent per item
    end

    if response[:next_cursor]
      # Chain the next page
      self.class.perform_in(1, response[:next_cursor])
    else
      Rails.logger.info(&quot;Sync complete ✅&quot;)
    end
  end
end

class UpsertExternalItem
  include Sidekiq::Worker

  def perform(raw)
    ExternalRecord.upsert(
      { external_id: raw[&quot;id&quot;], name: raw[&quot;name&quot;] },
      unique_by: :index_external_records_on_external_id
    )
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern lets you chew through millions of external records without drowning your system in retries or hitting API bans.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Sidekiq is incredibly robust, but at scale, you need production-grade practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep jobs &lt;strong&gt;small, fast, idempotent&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Prioritize with &lt;strong&gt;queues&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Monitor &lt;strong&gt;latency &amp;amp; retries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Tune &lt;strong&gt;concurrency &amp;amp; Redis&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;autoscaling&lt;/strong&gt; to survive traffic spikes&lt;/li&gt;
&lt;li&gt;Chain jobs for external API pagination to handle massive datasets gracefully&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With these practices, you can confidently handle &lt;strong&gt;millions of jobs per day&lt;/strong&gt; without breaking a sweat.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;✍️ Your turn: what’s your worst Sidekiq horror story? Did you also accidentally email your entire user base at 3am?&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/4983/scaling-sidekiq-in-ruby-on-rails-best-practices-war-stories-and-wtf-moments</guid>
<pubDate>Mon, 25 Aug 2025 00:27:16 +0000</pubDate>
</item>
</channel>
</rss>