Writing a Sidekiq Worker
Fast recall
Quick revision
Core answer
Sidekiq::Job, defines perform with small JSON-compatible arguments, and is enqueued with perform_async. Pass stable identifiers rather than live Active Record objects, reload current data inside the job, and make the external or database effect safe to repeat. Let unexpected failures raise so Sidekiq can record and retry them; turn expected business outcomes into explicit state instead of exceptions. The job payload is a durable interface, so argument changes must remain compatible with already queued work.Whether you know a worker is a class with perform + include Sidekiq::Job, args are JSON-serialized, and you pass IDs.
Passing objects/Active Records to perform (they don't survive JSON) — pass IDs and re-fetch.
A worker: include Sidekiq::Job; def perform(args); end. Args JSON-serialized (pass IDs). perform_async. Keep small + idempotent.
Interview-ready guide
Interview answer
A native Sidekiq job is a Ruby class that includes Sidekiq::Job and implements perform. Calling perform_async serializes the class, queue options, and arguments into a payload; a Sidekiq server later deserializes it and invokes perform.
Arguments should be simple JSON-compatible values such as strings, numbers, booleans, arrays, hashes with string keys, and nil. I normally pass an Active Record ID rather than an object, because native Sidekiq does not preserve a live model and the stored snapshot could be stale when the worker runs.
For example, ReceiptJob.perform_async(order.id) lets the worker reload the order, stop if a receipt was already delivered, generate a business idempotency key, send the message, and record completion. A retry after a timeout can then detect the completed effect instead of sending an uncontrolled duplicate.
Expected outcomes need deliberate handling. A cancelled order may simply return because no receipt is required. A temporary mail-provider timeout should raise so retry policy applies. Rescuing every exception and returning makes Sidekiq record false success and removes the job from normal failure visibility.
Job arguments form a versioned contract with work already in Redis. Adding a required positional argument can break payloads enqueued by the old deployment. Small payloads, keyword-like option hashes with string keys, tolerant readers, and staged deployments make job evolution safer.
Writing a Sidekiq Worker
Direct Answer
A native Sidekiq job includes Sidekiq::Job, defines perform with small JSON-compatible arguments, and is enqueued with perform_async. Pass stable identifiers rather than live Active Record objects, reload current data inside the job, and make the external or database effect safe to repeat. Let unexpected failures raise so Sidekiq can record and retry them; turn expected business outcomes into explicit state instead of exceptions. The job payload is a durable interface, so argument changes must remain compatible with already queued work.
Deep Dive & Architecture
A queued payload can outlive the request, the process, and the deployment that created it. It should contain enough stable information to locate the work without serializing incidental application state. Reloading an ID sees current truth; storing a deliberate immutable value is appropriate only when the historical snapshot itself is the contract.
Idempotency is about the business effect, not whether Ruby executes twice. A database uniqueness constraint can protect one receipt record, and an external API idempotency key can protect one provider action. A boolean checked before sending can still race unless its read and write share a safe boundary.
Exceptions communicate unexpected failure to Sidekiq. Broad rescue inside perform should either translate to a more meaningful exception or complete an explicit compensating state; silent logging loses retry and alerting. Timeouts should be bounded so one worker thread does not remain occupied indefinitely.
Testing separates the enqueue contract from execution. One test can assert that the correct job and simple arguments are enqueued after the business event. Another can run perform directly with controlled records and prove success, duplicate execution, missing data, and external failure behaviour.
Code Example
class ReceiptJob
include Sidekiq::Job
sidekiq_options queue: 'mailers'
def perform(order_id)
order = Order.find(order_id)
return if order.receipt_sent?
ReceiptDelivery.call(
order: order,
idempotency_key: "order-receipt-#{order.id}"
)
order.update!(receipt_sent_at: Time.current)
end
end
ReceiptJob.perform_async(order.id)
Common Mistakes & Pitfalls
Passing objects/Active Records to perform (they don't survive JSON) — pass IDs and re-fetch.
Check your understanding
Answer these without looking back at the article.
- Why should a native Sidekiq job normally receive an ID instead of a model object?
- How can a worker still duplicate an effect after checking a boolean flag?
- Why can changing job arguments break work already in the queue?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.