Wurk β€” an orc ready to work

Wurk ⚑

Wurk, wurk. πŸͺ“ Ready to work. Zug zug.

A 100% drop-in replacement for Sidekiq + Sidekiq Pro + Sidekiq Enterprise. Free forever. Faster.

Live Demo Gem Version CI Coverage gate Ruby License: MIT

Wurk is wire-compatible with Sidekiq β€” same Redis keys, same job JSON, same Ruby DSL. Swap one line in your Gemfile and your existing jobs, batches, limiters, cron entries, and live Redis data keep working untouched. The Pro and Enterprise feature sets ship in the same free gem, with no license check and no tiers.

Install

# Gemfile
gem "wurk"
# ...or drop in over an existing Sidekiq stack β€” delete these, add one line:
- gem "sidekiq"
- gem "sidekiq-pro", source: "https://gems.contribsys.com/"
- gem "sidekiq-ent", source: "https://enterprise.contribsys.com/"
+ gem "wurk"

bundle install && restart. That's it β€” Sidekiq::Worker, Sidekiq::Batch, Sidekiq::Limiter, Sidekiq.configure_server, and friends all resolve to Wurk.

Feature matrix

Everything below is in the one free gem. The "Sidekiq tier" column is only there to show what you'd otherwise pay for.

Area What you get Sidekiq tier
Runtime Fork-based real parallelism, reliable BLMOVE fetch, PID supervision, rolling restarts, graceful drain, scheduled/retry pollers OSS + Pro
Batches Sidekiq::Batch with on(:success/:complete/:death) callbacks, nested batches, progress Pro
Limiters Concurrent, bucket, window, leaky, and points rate limiters via Sidekiq::Limiter Enterprise
Periodic Cron/periodic jobs, leader-elected so each tick fires exactly once across the cluster Enterprise
Encryption Transparent AES-256-GCM job-argument encryption with zero-downtime key rotation Enterprise
Dashboard Mountable Rails engine, precompiled SolidJS SPA (no Node needed), live SSE, charts, host-app auth hook OSS + Pro/Ent

Plus Wurk extras: a worker topology DSL, a Kubernetes liveness/readiness listener, and opt-in AI dashboard panes (anomaly detection, NL queries, backlog forecasting).

Documentation

Requirements

Component Minimum
Ruby >= 3.2.0
Redis >= 7.0.0

JRuby, TruffleRuby, and Windows fall back to threads-only mode (no fork) β€” behaviorally equivalent to stock Sidekiq.

Running the workers

Under Rails the engine auto-starts the swarm on boot β€” a plain rails server already forks workers and fetches. Set WURK_DISABLED=1 on any process that shouldn't (e.g. the web tier when you run workers on their own dyno).

One exception β€” preforking web servers. A clustered Puma, Unicorn, or Passenger forks its own web workers, so Wurk refuses to also fork the swarm there β€” it would multiply the swarm by the web-worker count, or entangle its supervisor with the server's own fork/signal handling β€” and logs how to proceed. Either run the swarm as its own process (recommended):

bundle exec wurkswarm   # forked swarm, real parallelism

…or run it inside the web process as threads only, no fork (like Sidekiq embedded):

# config/application.rb  (here, not an initializer β€” server mode is decided before initializers load)
config.wurk.embed_in_web = true

Single-mode Puma (the rails server default) isn't preforking, so auto-start is unaffected. Full details, flags, and the standalone runners: Starting the worker.

The dashboard

Mount the engine wherever you like:

# config/routes.rb
mount Wurk::Engine => "/wurk"

The precompiled SPA ships inside the gem, so consumers never run Node. Gate it behind your app's auth with one line β€” see Securing the dashboard for Devise/Warden/Sorcery recipes:

Wurk::Web.use(Rack::Auth::Basic, "Wurk") { |user, pass| user == ENV["WURK_USER"] && pass == ENV["WURK_PASS"] }

Ship a viewer-only board (e.g. a public demo) with no auth code at all by setting WURK_WEB_READ_ONLY=1 β€” every mutating request returns 403 and the SPA hides destructive actions.

Security notes

  • Wurk::Web.use and the authorization hook gate the dashboard's routes and JSON API β€” every controller under the engine mount goes through them.
  • /wurk-assets/* (the precompiled SPA's JS/CSS/font bundle) is served unauthenticated, by design. It's inserted into the host app's own middleware stack ahead of the engine's routes, so it never reaches Wurk::Web.use/authorization. This is safe because the bundle carries no data β€” no job payloads, no Redis reads, nothing per-user β€” it's a static shell, same trust model as any Rails app's public/assets. Everything data-bearing (stats, queues, jobs) is served by the JSON API, which is gated. If you need to hide even the existence of the bundle (e.g. compliance requires the mount path itself stay secret), put a reverse-proxy rule in front of /wurk-assets rather than relying on the engine.
  • Redis being unreachable surfaces to the SPA as a structured 503 {"error": "redis_unavailable"} (JSON endpoints) or an SSE error event (the live stream), never a raw 500 β€” the client can branch on it instead of parsing an HTML error page.

Encryption

A drop-in for Sidekiq::Enterprise::Crypto. It encrypts the last positional argument of a job with AES-256-GCM β€” the client middleware seals it on push, the server middleware opens it before perform. Earlier args stay plaintext so you can still triage on user_id.

# config/initializers/wurk.rb β€” point at any key source (file, ENV, KMS)
Sidekiq::Enterprise::Crypto.enable(active_version: 1) do |version|
  File.binread("config/crypto/secret.#{Rails.env}.#{version}.key") # exactly 32 bytes
end
class ChargeCardJob
  include Sidekiq::Job
  sidekiq_options encrypt: true

  def perform(user_id, secret_bag) # secret_bag arrives already decrypted
    Payments.charge(user_id, secret_bag["pan"], secret_bag["cvv"])
  end
end

Keys rotate without downtime β€” keep every still-in-flight version resolvable so old jobs decrypt, then bump active_version. A job that can't be decrypted (key rotated away, corrupt ciphertext) goes straight to the dead set in under a second rather than crash-looping through 25 retries, with the still-encrypted payload preserved for replay. The dashboard renders encrypted args as "<encrypted>"; cleartext is never written to Redis.

Kubernetes probes

Opt in to a thin HTTP listener for liveness/readiness:

Wurk.configure_server do |config|
  config.health_check(port: 7433)
end
Path Meaning
/live 200 while the Launcher is running; 503 once stop/quiet is called.
/ready 200 only when Redis is reachable and the heartbeat fired within ready_window (default 30s); 503 otherwise.

Knobs: health_check(port:, bind: "0.0.0.0", ready_window: 30). In swarm mode only the first child to start binds the port.

Migrating from Sidekiq

- gem "sidekiq"
- gem "sidekiq-pro", source: "https://gems.contribsys.com/"
- gem "sidekiq-ent", source: "https://enterprise.contribsys.com/"
+ gem "wurk"

bundle install && restart. Wurk reads and writes the same Redis schema, so a rolling deploy can run Sidekiq and Wurk against the same Redis during the cutover. Third-party gems (sidekiq-cron, sidekiq-unique-jobs, sidekiq-scheduler, sidekiq-status, sidekiq-failures, sidekiq-throttled, …) are exercised by running their own upstream suites against Wurk in the ecosystem CI job (see test/ecosystem/).

Full walkthrough β€” config side-by-side, the Redis key/sidekiq_options mapping, known incompatibilities, and a one-page cutover checklist: docs/migrate-from-sidekiq.md.

Contributing

Issues and pull requests are welcome β€” see CONTRIBUTING.md for the dev setup, test layers, and conventions, and SECURITY.md to report a vulnerability.

License

MIT. See LICENSE.

Wurk is a clean-room reimplementation of the Sidekiq API β€” it copies the interface (so your jobs run unchanged), not Sidekiq's implementation code. This is the same basis the Supreme Court upheld for Google's reuse of the Java API in Google v. Oracle (2021). "Sidekiq" is a trademark of Contributed Systems, LLC; Wurk is independent and not affiliated with or endorsed by them. Full reasoning: docs/clean-room.md.