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
endYAML 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
endLimitation
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
endAdding 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
endHeartbeat 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}"
endFrequently 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
Quick-reference guide to cron expression syntax with common schedule examples.
Cron Job Not Running? 12 Common CausesDiagnose and fix the most common reasons cron jobs fail silently.
Cron Job Monitoring Best PracticesHeartbeat checks, log aggregation, alerting, and dashboards for cron jobs.
Node.js Cron Jobs GuideSchedule tasks in Node.js with node-cron, Agenda.js, and BullMQ.
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.