Sidekiq Reliable Scheduler

BackendQueuesMessaging
Share on LinkedIn Share on X Share on Reddit Share on HN Share on Bluesky

Subscription renewals stopped shipping on March 3rd because the Sidekiq scheduler pod had been OOMKilled for eleven days — and nobody noticed the empty schedule poll metric. Jobs were still in Redis, waiting for a process to call enqueue on due timestamps. Sidekiq scheduling is reliable against process crashes if Redis persists and if you run a singleton scheduler with monitoring.

Redis data structures behind scheduling

Sidekiq keys (simplified):

schedule          ZSET  score=timestamp, member=job payload
retry             ZSET  backoff retries
dead              SET   exhausted failures
queue:default     LIST  ready jobs

If poller stops, schedule ZSET grows with due scores in the past — backlog executes in burst when scheduler returns.

Monitor:

Sidekiq::ScheduledSet.new.size
Sidekiq::Stats.new.scheduled_size

Alert when oldest due job age > 5 minutes.

sidekiq-cron setup

# config/schedule.yml
billing_daily:
  cron: "0 2 * * *"
  class: BillingDailyJob
  queue: critical
Sidekiq.configure_server do |config|
  config.on(:startup) do
    schedule = YAML.load_file(Rails.root.join('config/schedule.yml'))
    Sidekiq::Cron::Job.load_from_hash(schedule)
  end
end

Cron jobs re-register on Sidekiq server startup — not on web pods.

Dynamic scheduling from workers

class TrialReminderJob
  include Sidekiq::Job

  def perform(user_id, day)
    send_reminder(user_id, day)
    TrialReminderJob.perform_in(3.days, user_id, day + 1) if day < 7
  end
end

Use UTC consistently: Time.zone = 'UTC'.

Unique jobs to prevent duplicate schedules

class BillingDailyJob
  include Sidekiq::Job
  sidekiq_options lock: :until_executed, on_conflict: :log

  def perform
    InvoiceGenerator.run!
  end
end

Ensure lock Redis keys use same Redis as Sidekiq.

Death handlers for recurring failures

Sidekiq.configure_server do |config|
  config.death_handlers << ->(job, ex) do
    Sentry.capture_exception(ex, extra: { jid: job['jid'], class: job['class'] })
    PagerDuty.trigger('billing daily failed', job) if job['class'] == 'BillingDailyJob'
  end
end

Singleton scheduler and leader election

Anti-pattern: Sidekiq server with cron enabled on every worker replica → duplicate enqueues at 2:00 AM.

Run one sidekiq deployment with cron, separate high-memory workers for batch without cron enabled.

Clock skew and DST

billing_daily:
  cron: "0 2 * * * America/New_York"

Test DST boundaries. Use UTC for storage, local for display.

Redis persistence implications

Scheduled jobs survive Sidekiq restart only if Redis persists (AOF everysec typical). Ephemeral Redis in dev → false "scheduler unreliable" diagnosis.

Sidekiq Web and operational visibility

Mount Sidekiq::Web behind auth. Tabs that matter: Scheduled, Retries, Dead, Cron (last enqueue time).

Catch-up behavior after long outage

Document business decision: skip vs burst catch-up missed cron runs when scheduler down > 24h.

Metrics export with Yabeda or Prometheus

Wire Grafana alert: sidekiq_scheduled_latency_seconds p99 > 300 during business hours.

Argument serialization and schedule size

Keep args as IDs referencing database rows — multi-megabyte JSON in scheduled job slows Redis poll loop.

Reliable scheduler checklist

Sidekiq's scheduler is durable because Redis is durable — not because polling is magic. Run one leader, lock duplicates, alert on death and overdue schedules, and rehearse what happens when eleven days of renewals enqueue in one minute.

sidekiq-scheduler vs perform_at load

High-volume perform_at from user actions (remind me later) fills schedule ZSET — distinct from cron. Monitor schedule size separately from cron job count. Archive old scheduled jobs or use Redis TTL patterns for canceled reminders to prevent unbounded ZSET growth.

Redis memory fragmentation

Sidekiq schedule entries serialize job args — memory fragmentation after burst schedule causes Redis OOM before maxmemory apparent full. Enable active defrag in Redis 4+ during maintenance window if schedule-heavy app shows RSS >> used_memory.

Sidekiq Enterprise periodic vs cron

Enterprise periodic jobs integrate leader election — worth license when sidekiq-cron duplicate enqueue causes financial incident. Evaluate after first duplicate billing cron incident cost exceeds license.

Graceful quiet before deploy

Run sidekiqctl quiet before deploy stops new job fetch; scheduler may still enqueue — quiet workers only. For zero duplicate cron during deploy, pause cron jobs in Sidekiq Web before pod termination if deploy spans cron minute boundary.

Scheduled job idempotency

Cron enqueues even when previous run still active unless unique lock — long-running nightly job overlapping next cron creates duplicate parallel runs. Extend unique lock duration or use lock: :while_executing on job class.

Kubernetes CronJob vs Sidekiq cron

Some teams run K8s CronJob that enqueues Sidekiq job instead of sidekiq-cron — duplicates scheduler responsibility. Pick one: Sidekiq-native cron for job args in Ruby ecosystem, K8s CronJob only when Sidekiq unavailable. Dual cron double-charges customers.

Redis ACL and Sidekiq scheduler

Redis 6 ACL limits keys Sidekiq uses — scheduler role needs access to schedule, queues, and queue lists. Overly restrictive ACL causes silent schedule poll failures with generic connection errors — test ACL in staging with exact production prefix.

Daylight saving and cron drift

Sidekiq-cron uses Fugit parser — invalid cron strings fail at load with error in logs during deploy. Add CI validation of schedule.yml schema and cron parse before merge.

Horizontal scaling Sidekiq without cron

Worker replicas scale horizontally; only scheduler pod loads cron — HPA on worker deployment must exclude scheduler deployment from same manifest template. Template mistake duplicates cron on every HPA scale event before unique lock catches duplicate.

Sidekiq Pro super_fetch reliability

Sidekiq Pro super_fetch reduces job loss on process crash — scheduler reliability separate from fetch reliability. Enterprise customers combine reliable scheduler with super_fetch so cron enqueue and worker fetch both survive Redis hiccups; evaluate bundle for financial cron paths.

Monitoring Redis schedule key memory

MEMORY USAGE on schedule key during growth incident — identifies runaway perform_at from bug looping schedule without delete. Fix bug then ZREM orphaned entries or restart from known good backup if schedule corrupted.

Runbook: scheduler pod not running

Step 1: verify Sidekiq process up. Step 2: check Redis schedule ZSET size and oldest score. Step 3: check sidekiq-cron last enqueue. Step 4: manual Sidekiq::Cron::Job.find(name).enque! for missed critical cron once after fix. Step 5: postmortem why alert did not fire.

Billing cron idempotency audit

Before enabling new billing cron, QA verifies duplicate enqueue produces single charge — Stripe idempotency keys plus Sidekiq unique lock plus death handler page on any duplicate invoice ID in database unique constraint violation log.

Closing principle

Scheduled money movement without scheduler monitoring is a liability. One Redis, one cron leader, unique jobs on financial tasks, death handlers that page, and alerts on overdue schedule entries — minimum bar before calling Sidekiq scheduling production-ready.

Read next when cron misfires

Compare Sidekiq Web cron last enqueue timestamp with expected cron — if stale, scheduler process is down while workers healthy. If enqueue fresh but job not running, queue routing or worker -Q subscription mismatch is next check before blaming cron expression typo.

Document tier ownership, DLX bindings, cron schedules, and FIFO group-key schema in the same repository as application code — operational knowledge drift causes repeat incidents when runbooks live only in wiki software nobody updates after reorganizations.

Include scheduler pod health in the same PagerDuty service as payment workers — siloed alerts let scheduler die quietly while workers process backlog that never replenishes from cron.

Verify cron timezone in staging by enqueuing test job one minute ahead — catches UTC versus local misconfiguration before billing window. Run cron enqueue from a single scheduler role and prove in staging with two pods that duplicate fires cannot happen; pair with uniqueness locks on the worker.

Frequently asked questions

How does Sidekiq schedule jobs for the future?

Sidekiq stores scheduled jobs in a Redis sorted set (schedule) scored by Unix timestamp. The Sidekiq scheduler process polls due scores and pushes jobs to the appropriate queue lists. If scheduler process is down, jobs run late but remain in Redis — they are not lost unless Redis loses data.

What is the difference between sidekiq-scheduler, sidekiq-cron, and Sidekiq Enterprise periodic jobs?

sidekiq-cron loads cron YAML into Redis and enqueues on schedule. sidekiq-scheduler adds dynamic rescheduling from worker code. Sidekiq Enterprise provides Periodic Jobs with leader election built in. All require exactly one scheduler leader per Redis to avoid duplicate enqueues unless unique jobs guard duplicates.

How do Sidekiq death handlers relate to scheduled jobs?

Death handlers run when a job exhausts retries and lands in the Dead set. For scheduled/recurring jobs, a death handler prevents silent failure — log, alert, or re-enqueue next interval. Without handlers, nightly billing may fail for weeks before anyone notices empty invoices.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →