Migrate from Sidekiq
If you have been running Sidekiq in production, you already understand background job processing. OJS builds on many of the same ideas that Sidekiq pioneered (simple args, server-side retry, middleware chains), but makes them language-agnostic and backend-portable. This guide maps Sidekiq concepts to OJS equivalents and walks through a step-by-step migration.
Concept mapping
Section titled “Concept mapping”| Sidekiq | OJS | Notes |
|---|---|---|
Sidekiq::Worker / include Sidekiq::Job | Handler function registered with worker | OJS handlers are plain functions, not classes |
perform_async(args...) | client.enqueue(type, args) | Both use JSON-native arrays for args |
Sidekiq.redis | OJS backend server | OJS abstracts the storage layer behind an HTTP API |
Sidekiq::Queue | OJS queue (named, server-managed) | Same concept, same "default" default |
sidekiq_options queue: "email" | client.enqueue("email.send", args, queue: "email") | Queue is per-enqueue, not per-class |
sidekiq_retry_in | Retry policy on the job envelope | Server-side, configurable per job |
perform_in(5.minutes, args) | client.enqueue(type, args, delay: "5m") | OJS uses scheduled_at or delay helpers |
Sidekiq::Cron::Job | client.register_cron(...) | OJS has native cron support at Level 2 |
| Dead set / Retries tab | Dead letter queue + discarded state | OJS has structured error info per attempt |
| Sidekiq Pro batch | batch() workflow primitive | OJS batch includes on_complete, on_success, on_failure |
Sidekiq.server_middleware | worker.use (execution middleware) | Same next() pattern |
Sidekiq.client_middleware | client.enqueue_middleware | Same next() pattern |
Code comparison
Section titled “Code comparison”Sidekiq worker class
Section titled “Sidekiq worker class”# Sidekiqclass EmailWorker include Sidekiq::Job
sidekiq_options queue: "email", retry: 5
def perform(to, template) EmailService.send(to: to, template: template) endend
# EnqueueEmailWorker.perform_async("user@example.com", "welcome")OJS handler registration
Section titled “OJS handler registration”# OJS Ruby SDKrequire "ojs"
# Create client and workerclient = OJS::Client.new("http://localhost:8080")worker = OJS::Worker.new("http://localhost:8080", queues: %w[email default])
# Register handler (plain block, not a class)worker.register("email.send") do |ctx| to = ctx.job.args[0] template = ctx.job.args[1] EmailService.send(to: to, template: template) { status: "sent" }end
# Enqueueclient.enqueue("email.send", ["user@example.com", "welcome"], queue: "email", retry: OJS::RetryPolicy.new(max_attempts: 5))
# Start the worker (blocks)worker.startThe main structural difference: Sidekiq uses classes with perform methods. OJS uses plain functions (or blocks) registered by job type name. There is no need for a class hierarchy.
Key differences
Section titled “Key differences”Args format
Section titled “Args format”Both Sidekiq and OJS use JSON arrays for job arguments. Sidekiq’s perform(to, template) maps to OJS args: ["user@example.com", "welcome"]. This is one of Sidekiq’s best design decisions, and OJS adopted it directly.
If you follow Sidekiq’s best practice of keeping args as simple JSON types (strings, numbers, booleans), your args will work in OJS without changes.
8-state lifecycle vs. Sidekiq’s implicit states
Section titled “8-state lifecycle vs. Sidekiq’s implicit states”Sidekiq tracks jobs across Redis sorted sets (queued, busy, retries, dead, scheduled), but the states are implicit. OJS makes every state explicit and documents all valid transitions:
scheduled -> available -> active -> completed -> retryable -> available (retry) -> discarded (retries exhausted) -> cancelled (manual cancel)This means you can always query a job’s exact state and get a clear answer about what is happening.
Retry policies are per-job, not per-class
Section titled “Retry policies are per-job, not per-class”In Sidekiq, retry settings live on the worker class:
# Sidekiq: retry config is class-levelsidekiq_options retry: 5, retry_in: 30In OJS, retry settings are part of the job envelope. You set them at enqueue time:
# OJS: retry config is per-enqueueclient.enqueue("email.send", ["user@example.com"], retry: OJS::RetryPolicy.new( max_attempts: 5, initial_interval: "PT1S", backoff_coefficient: 2.0, max_interval: "PT5M", jitter: true, non_retryable_errors: ["ValidationError"] ))This is more flexible. The same job type can have different retry policies depending on the context.
Cross-language support
Section titled “Cross-language support”Sidekiq is Ruby-only. OJS jobs are language-agnostic JSON. You can enqueue a job from a Python service and process it with a Ruby worker, or vice versa.
Structured error reporting
Section titled “Structured error reporting”Sidekiq stores the last error message as a string. OJS stores structured errors with a type, message, and backtrace for every failed attempt:
{ "errors": [ { "type": "SmtpConnectionError", "message": "Connection refused to smtp.example.com:587", "backtrace": ["at SmtpClient.connect (smtp.rb:42)"], "attempt": 1, "failed_at": "2026-02-12T10:30:00Z" } ]}Step-by-step migration
Section titled “Step-by-step migration”Step 1: Start the OJS backend
Section titled “Step 1: Start the OJS backend”Run the OJS Redis backend alongside your existing Sidekiq Redis. OJS uses its own key namespace, so they do not conflict even on the same Redis instance.
services: redis: image: redis:7-alpine ports: - "6379:6379"
ojs-server: image: ghcr.io/openjobspec/ojs-backend-redis:latest ports: - "8080:8080" environment: REDIS_URL: redis://redis:6379 depends_on: - redisdocker compose up -dcurl http://localhost:8080/ojs/v1/health# {"status":"ok"}Step 2: Install the Ruby SDK
Section titled “Step 2: Install the Ruby SDK”Add the OJS Ruby SDK to your Gemfile:
# Gemfilegem "ojs"bundle installStep 3: Create an OJS client and worker
Section titled “Step 3: Create an OJS client and worker”require "ojs"
OJS_CLIENT = OJS::Client.new(ENV.fetch("OJS_URL", "http://localhost:8080"))Step 4: Convert worker classes to OJS handlers
Section titled “Step 4: Convert worker classes to OJS handlers”For each Sidekiq worker, create an equivalent OJS handler. You can do this incrementally, one worker at a time.
Before (Sidekiq):
class WelcomeEmailWorker include Sidekiq::Job sidekiq_options queue: "email"
def perform(user_id) user = User.find(user_id) Mailer.welcome(user).deliver_now endendAfter (OJS):
OJS_WORKER = OJS::Worker.new( ENV.fetch("OJS_URL", "http://localhost:8080"), queues: %w[email default], concurrency: 10)
OJS_WORKER.register("email.welcome") do |ctx| user_id = ctx.job.args[0] user = User.find(user_id) Mailer.welcome(user).deliver_now { user_id: user_id, status: "sent" }endStep 5: Update enqueue calls
Section titled “Step 5: Update enqueue calls”Before:
WelcomeEmailWorker.perform_async(user.id)After:
OJS_CLIENT.enqueue("email.welcome", [user.id], queue: "email")Step 6: Run both systems in parallel
Section titled “Step 6: Run both systems in parallel”During the migration, keep both Sidekiq and OJS running. Migrate one job type at a time:
- Convert the worker class to an OJS handler.
- Update the enqueue calls.
- Deploy and verify the job processes correctly.
- Remove the old Sidekiq worker class.
This approach lets you roll back individual job types if something goes wrong.
Step 7: Convert middleware
Section titled “Step 7: Convert middleware”Sidekiq server middleware:
class LoggingMiddleware def call(worker, job, queue) start = Time.now yield puts "#{worker.class} done in #{Time.now - start}s" endend
Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add LoggingMiddleware endendOJS execution middleware:
OJS_WORKER.use("logging") do |ctx, &nxt| start = Time.now result = nxt.call puts "#{ctx.job.type} done in #{Time.now - start}s" resultendStep 8: Migrate scheduled and cron jobs
Section titled “Step 8: Migrate scheduled and cron jobs”Sidekiq scheduled jobs:
# BeforeWelcomeEmailWorker.perform_in(1.hour, user.id)
# AfterOJS_CLIENT.enqueue("email.welcome", [user.id], queue: "email", delay: "1h")Sidekiq-Cron jobs:
# BeforeSidekiq::Cron::Job.create(name: "daily-report", cron: "0 9 * * *", class: "DailyReportWorker")
# AfterOJS_CLIENT.register_cron( name: "daily-report", cron: "0 9 * * *", timezone: "America/New_York", type: "report.daily", args: [])Step 9: Cutover and remove Sidekiq
Section titled “Step 9: Cutover and remove Sidekiq”Once all job types are migrated and running smoothly on OJS:
- Remove
sidekiqandsidekiq-cronfrom your Gemfile. - Remove Sidekiq worker classes and configuration.
- Shut down Sidekiq processes.
What you gain
Section titled “What you gain”- Language interoperability. Your Ruby app can enqueue jobs that a Go or Python service processes. Useful for gradual language migrations or polyglot architectures.
- Backend portability. Switch from Redis to PostgreSQL without changing application code. Useful if you want SQL-level durability guarantees.
- Structured error history. Every failed attempt gets a full error record, not just the last error message.
- Conformance testing. The OJS conformance suite verifies that your backend behaves correctly. No more guessing about edge cases.
- Standardized retry policies. Exponential backoff with jitter, non-retryable error classification, and per-job configuration are all built in.
- Workflow primitives. Chain, group, and batch give you Sidekiq Pro batch-like functionality (and more) as part of the standard.