ruby 66 lines · 3 tabs

Safer Background Job Arguments (Serialize IDs only)

Shared by codesnips Jan 2026
3 tabs
class NotifyFollowersJob
  include Sidekiq::Job

  sidekiq_options queue: :notifications, retry: 5

  def perform(post_id, actor_id)
    post = Post.find_by(id: post_id)
    return unless post

    actor = User.find_by(id: actor_id)
    return unless actor

    post.author.followers.find_each do |follower|
      dedupe_key = "notify:#{post_id}:#{follower.id}"
      next if Notification.exists?(dedupe_key: dedupe_key)

      Notification.create!(
        dedupe_key: dedupe_key,
        recipient: follower,
        actor: actor,
        notifiable: post,
        kind: :new_post
      )
    end
  end
end
3 files · ruby Explain with highlit

Sidekiq serializes job arguments to JSON and stores them in Redis until a worker picks them up. This means whatever gets pushed into perform_async must survive a round trip through JSON, and it may sit in the queue for seconds or minutes before running. Passing a full ActiveRecord object is a classic mistake: the object gets frozen at enqueue time, so the worker operates on a stale snapshot of the record — or worse, on a serialized blob that no longer matches the row in the database. The safe convention is to pass only primitive identifiers and re-load the record fresh inside the job.

The NotifyFollowersJob tab shows the pattern. perform_async is called with an integer post_id rather than the Post itself, and the perform method opens by calling Post.find_by(id: post_id). The return unless post guard handles the important edge case where the row was deleted between enqueue and execution — a real possibility given queue latency. Fetching inside the job guarantees the worker sees the current state, including any updates made after the job was scheduled. The job also derives an idempotency key from the passed IDs so a retried delivery does not double-send.

The Post model tab centralizes enqueueing in notify_followers_later, which the model calls from an after_commit hook. Using after_commit rather than after_save matters: the job is only queued once the transaction is durably committed, so the worker can never look up a record that does not yet exist. The method passes id, not self.

The PostsController tab wires this into a normal request. On successful @post.save, notify_followers_later runs via the callback; the controller itself stays thin and never touches Sidekiq directly.

The trade-off is one extra database read per job, which is cheap and almost always worth the correctness it buys. The rule of thumb is to treat job arguments like URL parameters — small, primitive, and meaningless without a fresh lookup. Avoid passing hashes of attributes too, since they drift out of sync just like whole records. This keeps jobs replayable, retry-safe, and immune to stale-data bugs.


Related snips

Share this code

Here's the card — post it anywhere.

Safer Background Job Arguments (Serialize IDs only) — share card
Link copied