Skip to content

About

Display sidekiq errors grouped by class and job name

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

sidekiq-error-groups

Gem Version CI Sidekiq License: MIT

An Error groups tab for the Sidekiq Web UI: retrying and dead jobs grouped by error class, by job class, or by the pair of them — with retry, kill and delete scoped to one group.

The Error groups tab, grouped by error class and job class

Why

Sidekiq's Retries and Dead pages are flat lists sorted by time. With 20 000 failed jobs they cannot answer the only question that matters — what broke? — and the bulk actions are all-or-nothing.

And grouping by error class alone is not enough either. Net::OpenTimeout in FetchInventoryJob is a flaky warehouse API; the same Net::OpenTimeout in DeliverWebhookJob is a flaky customer endpoint. Two problems, two decisions. Pairing the error class with the job class keeps them apart, so you can retry one and leave the other alone. That is the default grouping; the other two are one click away.

Nothing is stored and nothing is instrumented: the tab reads the very same retry and dead sets Sidekiq already keeps, so it works on failures that happened before you installed it.

Installation

# Gemfile
gem "sidekiq-error-groups"

Require the Web extension where the Web UI is mounted — config/routes.rb in Rails, or your rackup file. Not in an initializer: Sidekiq worker processes should not load the Web UI.

# config/routes.rb
require "sidekiq/web"
require "sidekiq/error_groups/web"

Rails.application.routes.draw do
  mount Sidekiq::Web => "/sidekiq"
end

That is the whole setup. A new "Error groups" tab appears next to Retries and Dead.

The tab

Index — one row per group, with a Group by: error class · job · error class + job switch. Each row carries the totals per set, when the group first and last failed, and the message of its most recent failure. When the grouping leaves something out, it comes back as a breakdown column: job classes when grouping by error, error classes when grouping by job.

Grouped by error class, with the affected job classes as a breakdown

Group page — the numbers for that group, the message and backtrace of the most recent failure, the three bulk actions, and a paginated list of the jobs linking back to their regular Sidekiq pages.

A single group: counts, backtrace, bulk actions and the affected jobs

Action Effect
Retry all in group pushes every job of this group back to its queue (retries and dead)
Kill all in group moves this group's retrying jobs to the dead set
Delete all in group deletes every job of this group (retries and dead)

Each action asks for confirmation, stays inside the group it was fired from, and reports how many jobs it touched.

Ruby API

summary = Sidekiq::ErrorGroups.summary        # grouped by error class + job class
summary.total          # => 72   failed jobs in the scanned sets
summary.group_by       # => :error_job
summary.truncated?     # => false (true when max_scan stopped the scan early)

summary.groups.each do |group|
  group.error_class    # => "Net::OpenTimeout"
  group.job_class      # => "FetchInventoryJob"
  group.count          # => 8
  group.retry_count    # => 8
  group.dead_count     # => 0
  group.first_failed_at
  group.last_failed_at
  group.sample         # => the group's most recent failed job
end

Other groupings, and the breakdown of whatever is not in the key:

Sidekiq::ErrorGroups.summary(group_by: :error)   # one row per error class
Sidekiq::ErrorGroups.summary(group_by: :job)     # one row per job class
Sidekiq::ErrorGroups.summary(sets: [:dead])      # dead jobs only

group = Sidekiq::ErrorGroups.groups(group_by: :job).first
group.error_classes    # => {"Net::OpenTimeout" => 5, "Net::ReadTimeout" => 5}
group.breakdown        # => [["Net::OpenTimeout", 5], ["Net::ReadTimeout", 5]]

One group's jobs, paginated (page.group carries the whole-group numbers, not just the page's):

page = Sidekiq::ErrorGroups.jobs("Net::OpenTimeout", job_class: "FetchInventoryJob", page: 1)
page.jobs.first.jid

Bulk actions, scoped to whichever half of the key you pass:

Sidekiq::ErrorGroups.retry_all("Net::OpenTimeout", job_class: "FetchInventoryJob")
Sidekiq::ErrorGroups.delete_all("ActiveRecord::RecordNotFound", sets: [:dead])
Sidekiq::ErrorGroups.kill_all(job_class: "WarmCacheJob", limit: 100)

An action with neither half raises ArgumentError rather than quietly touching every failed job. Jobs carrying no error_class are grouped under Sidekiq::ErrorGroups::Selector::UNKNOWN and displayed as "(no error class)".

Rake and CLI

bundle exec rake sidekiq:error_groups    # GROUP_BY=error_job SETS=retry,dead LIMIT=20 MAX_SCAN=10000
$ sidekiq-error-groups --group-by job
Failed jobs grouped by job class — 72 job(s) (retry: 62, dead: 10), 7 group(s)

JOB                TOTAL  RETRY  DEAD  LAST FAILURE             ERRORS
-----------------  -----  -----  ----  -----------------------  ----------------------------------------------------------
ChargeCardJob      30     30     0     2026-10-06 08:48:05 UTC  PaymentGateway::DeclinedError (26), …RateLimitedError (4)
DeliverWebhookJob  10     10     0     2026-10-06 08:46:12 UTC  Net::OpenTimeout (5), Net::ReadTimeout (5)
SyncUserJob        9      9      0     2026-10-06 08:30:41 UTC  ActiveRecord::RecordNotFound (9)
sidekiq-error-groups                                   # the default table
sidekiq-error-groups --group-by error                  # or job, or error_job
sidekiq-error-groups --json | jq .                     # machine readable
sidekiq-error-groups --error "Net::OpenTimeout"        # the jobs of one error class
sidekiq-error-groups --job FetchInventoryJob           # the jobs of one worker
sidekiq-error-groups --error "Net::OpenTimeout" --job FetchInventoryJob
sidekiq-error-groups -r ./config/environment           # load the app first
sidekiq-error-groups --redis-url redis://localhost:6379/1

The CLI is read-only on purpose: bulk retry/delete/kill live in the Ruby API and in the Web UI, where there is a confirmation step.

Configuration

Everything is optional.

Sidekiq::ErrorGroups.configure do |config|
  config.group_by = :error_job   # :error, :job or :error_job (default)
  config.sets = %i[retry dead]   # which sets to scan, default both
  config.max_scan = 10_000       # max jobs read per set, the safety valve for huge sets
  config.action_batch_size = 1_000
  config.top_breakdown = 3       # entries listed in the breakdown column
  config.per_page = 25           # jobs per page on the group page
  config.backtrace_lines = 10
  config.cache_ttl = 0           # seconds to keep a summary in process memory, 0 = off
end

Performance

Grouping means reading the sets, so the work is linear in their size. Iteration goes through Sidekiq::JobSet#each, which paginates the sorted set inside Redis, and stops after max_scan jobs per set. When that limit kicks in, the UI, the CLI and Summary#truncated? all say so — the numbers are then a lower bound. Raise max_scan for exact numbers on large installations, or set cache_ttl to avoid rescanning on every page load.

Bulk actions deliberately ignore max_scan: a "retry all in group" must not silently skip jobs. They work in batches of action_batch_size, re-scanning the set between batches, because mutating a sorted set while iterating it is not safe.

Compatibility

Ruby 2.7+ and Sidekiq 6.0 through 8.x. The Web UI extension API changed three times in that range; the gem shims all of it and CI runs the full matrix.

Sidekiq Registration used Views
8.x Sidekiq::Web.configure { |c| c.register(...) } web/views (current UI markup)
7.3–7.x Sidekiq::Web.register(ext, name:, tab:, index:) web/views/legacy
6.0–7.2 Sidekiq::Web.register(ext) + Sidekiq::Web.tabs[...] web/views/legacy

Only Sidekiq APIs present in every supported version are used (Sidekiq::RetrySet, Sidekiq::DeadSet, JobSet#each, SortedEntry#item/score/retry/delete/kill), and Redis is never addressed directly — which keeps both redis-rb (Sidekiq 6) and redis-client (Sidekiq 7+) happy. Groups are addressed with error_class/job_class query parameters rather than path segments, which every Sidekiq router version handles unchanged.

Locales: English and Russian.

Development

bin/setup
bundle exec rake                 # tests + rubocop

The core is tested against in-memory fakes and needs nothing running. The Web UI tests drive a real Sidekiq::Web through rack-test and need Redis; they skip themselves when REDIS_URL (default redis://localhost:6379/15) is unreachable.

The whole version matrix:

for f in gemfiles/*.gemfile; do BUNDLE_GEMFILE=$f bundle install && BUNDLE_GEMFILE=$f bundle exec rake test; done

Looking at the UI by hand:

bundle exec ruby test/dummy/seed.rb                             # create failed jobs
bundle exec rackup test/dummy/config.ru -p 9292                 # current UI
BUNDLE_GEMFILE=gemfiles/sidekiq_6.5.gemfile bundle exec rackup test/dummy/config.ru -p 9293  # legacy UI

Bug reports and pull requests are welcome — see CONTRIBUTING.md.

License

MIT

About

Display sidekiq errors grouped by class and job name

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages