Back to Blog
Ruby8 min read

Ruby Cron Jobs: Whenever, Sidekiq-Cron & Rufus-Scheduler Guide

Ruby and Rails applications often need recurring tasks: sending digest emails, expiring old sessions, syncing data from third-party APIs, or generating nightly reports. This guide covers the most popular scheduling approaches in the Ruby ecosystem, from simple crontab generation to Redis-backed job queues, with production-ready code examples.

Whenever Gem: Ruby DSL for Crontab

Whenever is the most widely used scheduling gem in the Rails ecosystem. Instead of editing crontab files manually, you write schedules in a clean Ruby DSL. Whenever translates them into standard cron expressions and updates your system crontab.

gem install whenever
# or add to Gemfile:
gem 'whenever', require: false

Defining Schedules (schedule.rb)

Run wheneverize . to create config/schedule.rb, then define your tasks:

# config/schedule.rb
set :output, 'log/cron.log'
set :environment, 'production'

# Run a Rake task every day at 2:30 AM
every 1.day, at: '2:30 am' do
  rake 'reports:generate_daily'
end

# Run a custom runner every 6 hours
every 6.hours do
  runner 'DataSync.perform'
end

# Run a shell command every Monday at 9 AM
every :monday, at: '9am' do
  command 'bash /home/deploy/scripts/weekly_backup.sh'
end

# Use raw cron syntax for complex schedules
every '0 */4 * * 1-5' do
  rake 'cache:warm'
end

Writing to Crontab

# Preview generated cron entries (dry run)
whenever

# Write to crontab
whenever --update-crontab myapp

# Clear all entries for this app
whenever --clear-crontab myapp

Capistrano Integration

Whenever integrates with Capistrano for automatic crontab updates on deploy:

# Capfile
require 'whenever/capistrano'

# deploy.rb
set :whenever_identifier, -> { "myapp_#{fetch(:stage)}" }

Limitation

Whenever generates system crontab entries. On multi-server deployments, every server runs the same cron schedule, which can cause duplicate job execution. Use Sidekiq-Cron or an external scheduler for distributed environments.

Sidekiq-Cron: Redis-Backed Scheduling

Sidekiq-Cron adds cron scheduling on top of Sidekiq. Schedules are stored in Redis, so only one instance enqueues each job regardless of how many servers are running. It uses standard cron expressions and integrates with the Sidekiq web UI.

gem 'sidekiq-cron', '~> 2.0'

Defining Scheduled Jobs

# config/initializers/sidekiq_cron.rb
schedule = [
  {
    name: 'Daily Report',
    cron: '0 3 * * *',
    class: 'DailyReportWorker',
    queue: 'reports'
  },
  {
    name: 'Expire Sessions',
    cron: '*/15 * * * *',
    class: 'ExpireSessionsWorker'
  },
  {
    name: 'Sync Inventory',
    cron: '0 */2 * * *',
    class: 'InventorySyncWorker',
    args: { warehouse: 'main' }
  }
]

Sidekiq::Cron::Job.load_from_array!(schedule)

Worker Classes

# app/workers/daily_report_worker.rb
class DailyReportWorker
  include Sidekiq::Worker
  sidekiq_options queue: :reports, retry: 3

  def perform
    report = Report.generate_daily
    ReportMailer.daily(report).deliver_later
    Rails.logger.info "Daily report generated: #{report.id}"
  end
end

YAML Configuration

# config/sidekiq_cron.yml
daily_report:
  cron: '0 3 * * *'
  class: DailyReportWorker
  queue: reports

expire_sessions:
  cron: '*/15 * * * *'
  class: ExpireSessionsWorker
# config/initializers/sidekiq_cron.rb
schedule = YAML.load_file(
  Rails.root.join('config', 'sidekiq_cron.yml')
)
Sidekiq::Cron::Job.load_from_hash!(schedule)

Why Sidekiq-Cron?

Unlike Whenever, Sidekiq-Cron prevents duplicate execution on multi-server deployments because Redis ensures only one Sidekiq process enqueues each scheduled job. You also get Sidekiq's built-in retries, error tracking, and web dashboard.

Rufus-Scheduler: In-Process Scheduling

Rufus-Scheduler is a pure Ruby, in-process scheduler that supports cron syntax, intervals, and one-shot jobs. It does not depend on Redis, MongoDB, or the system crontab. Jobs run inside your Ruby process using threads.

gem 'rufus-scheduler', '~> 3.9'
require 'rufus-scheduler'

scheduler = Rufus::Scheduler.new

# Cron syntax
scheduler.cron '0 3 * * *' do
  DailyReport.generate
end

# Interval (every 30 minutes)
scheduler.every '30m' do
  HealthCheck.ping
end

# Prevent overlap
scheduler.every '10m', overlap: false do
  LongRunningSync.perform
end

scheduler.join

Rails Integration

# config/initializers/scheduler.rb
require 'rufus-scheduler'

unless defined?(Rails::Console) || Rails.env.test? ||
       File.split($PROGRAM_NAME).last == 'rake'
  scheduler = Rufus::Scheduler.singleton

  scheduler.every '1h' do
    Rails.logger.info 'Running hourly cleanup...'
    OldRecord.cleanup
  end
end

Limitation

Rufus-Scheduler is in-memory only. Jobs are lost when the process restarts. In multi-process setups (Puma cluster mode), each worker starts its own scheduler, causing duplicate job execution. Use Sidekiq-Cron for multi-process or multi-server environments.

Rake Tasks with Crontab

The simplest approach: write a Rake task and schedule it with the system cron job. No gem needed. This works well for single-server deployments where you want full control.

Creating a Rake Task

# lib/tasks/cleanup.rake
namespace :cleanup do
  desc 'Remove expired sessions older than 7 days'
  task expired_sessions: :environment do
    count = Session.where('updated_at < ?', 7.days.ago).delete_all
    puts "Deleted #{count} expired sessions"
  end

  desc 'Archive old orders'
  task archive_orders: :environment do
    Order.archivable.find_each do |order|
      order.archive!
    end
  end
end

Adding to Crontab

Use a cron expression generator to build the right schedule, then add it to your crontab:

# Edit crontab
crontab -e

# Run cleanup every day at 3 AM
0 3 * * * cd /home/deploy/myapp && RAILS_ENV=production bundle exec rake cleanup:expired_sessions >> log/cron.log 2>&1

# Archive orders every Sunday at 4 AM
0 4 * * 0 cd /home/deploy/myapp && RAILS_ENV=production bundle exec rake cleanup:archive_orders >> log/cron.log 2>&1

Tip

Always set RAILS_ENV, use absolute paths, and redirect output to a log file. Cron runs with a minimal environment, so missing PATH or environment variables is the number one cause of failed Rake cron jobs.

Clockwork: Standalone Clock Process

Clockwork runs as a separate long-running process (often managed by systemd or Heroku's clock dyno). It defines schedules in Ruby and triggers jobs at the right time. Unlike Whenever, it does not use the system crontab.

# clock.rb
require 'clockwork'
require_relative 'config/boot'
require_relative 'config/environment'

module Clockwork
  every(1.hour, 'cleanup.expired_sessions') do
    ExpireSessionsWorker.perform_async
  end

  every(1.day, 'reports.daily', at: '03:00') do
    DailyReportWorker.perform_async
  end

  every(15.minutes, 'health.check') do
    HealthCheckWorker.perform_async
  end
end
# Start the clock process
bundle exec clockwork clock.rb

# Procfile (Heroku)
clock: bundle exec clockwork clock.rb

Clockwork is a good fit for Heroku-style platforms where you cannot edit the system crontab. However, it has been largely superseded by Sidekiq-Cron, which provides the same functionality with better Redis integration and Sidekiq's built-in web UI.

Monitoring with CronJobPro

Regardless of which scheduling approach you use, you need monitoring to catch silent failures. A Rake task that exits with code 0 but processes zero records looks successful to cron. CronJobPro solves this with two patterns:

HTTP Trigger (Replace Cron)

Expose your task as a Rails endpoint and let CronJobPro call it on schedule. You get automatic retries, execution time tracking, and failure alerts.

# config/routes.rb
namespace :cron do
  post 'cleanup', to: 'tasks#cleanup'
  post 'daily_report', to: 'tasks#daily_report'
end

# app/controllers/cron/tasks_controller.rb
module Cron
  class TasksController < ApplicationController
    skip_before_action :verify_authenticity_token
    before_action :verify_cron_token

    def cleanup
      count = Session.expired.delete_all
      render json: { deleted: count }, status: :ok
    end

    private

    def verify_cron_token
      token = ENV['CRON_SECRET']
      head :unauthorized unless request.headers['Authorization'] == "Bearer #{token}"
    end
  end
end

Heartbeat Monitoring (Keep Existing Cron)

Keep your existing Whenever or Sidekiq-Cron setup but add a ping at the end of each job. CronJobPro alerts you if the ping does not arrive on time.

require 'net/http'

def ping_cronjobpro(monitor_id)
  uri = URI("https://cronjobpro.com/api/ping/#{monitor_id}")
  Net::HTTP.get(uri)
rescue => e
  Rails.logger.warn "CronJobPro ping failed: #{e.message}"
end

Frequently Asked Questions

What is the best way to schedule cron jobs in Ruby on Rails?

The most popular approach is the Whenever gem for single-server deployments. For distributed applications, Sidekiq-Cron prevents duplicate execution across servers. For external HTTP triggering, CronJobPro calls your Rails endpoints on a schedule.

How does the Whenever gem work?

Whenever reads a schedule.rb file with a Ruby DSL. When you run whenever --update-crontab, it translates your DSL into standard crontab entries.

Should I use Sidekiq-Cron or Whenever for Rails?

Use Sidekiq-Cron if you already use Sidekiq and need distributed-safe scheduling. Use Whenever for simpler single-server deployments where system-level cron is preferred.

Can I run Rake tasks on a cron schedule?

Yes. Add a crontab entry: cd /path/to/app && RAILS_ENV=production bundle exec rake task_name. The Whenever gem can generate these entries automatically.

How do I prevent overlapping cron jobs in Ruby?

Sidekiq-Cron handles concurrency via Redis. With system cron, use flock for file-based locking. Rufus-Scheduler supports overlap: false. CronJobPro waits for the previous request to finish.

Related Articles

Schedule your Ruby endpoints externally

CronJobPro calls your Rails API routes on schedule with automatic retries, monitoring, and alerts. No gems to install. Free for up to 5 jobs.