javan/whenever · error · ArgumentError

Time must be in minutes or higher

Error message

Time must be in minutes or higher

What it means

Whenever converts numeric frequencies into cron fields, and crontab syntax has no seconds field — its finest granularity is one minute. parse_time raises ArgumentError for any frequency below 60 seconds because such an interval simply cannot be expressed in cron. The check applies to the seconds range 0...60 after symbol-to-seconds conversion.

Source

Thrown at lib/whenever/cron.rb:93

        if shortcut.is_a?(Numeric)
          @time = shortcut
          parse_time
        elsif shortcut
          if @at.is_a?(Time) || (@at.is_a?(Numeric) && @at > 0)
            raise ArgumentError, "You cannot specify an ':at' when using the shortcuts for times."
          else
            return shortcut
          end
        else
          parse_as_string
        end
      end

      def parse_time
        timing = Array.new(5, '*')
        case @time
          when Whenever.seconds(0, :seconds)...Whenever.seconds(1, :minute)
            raise ArgumentError, "Time must be in minutes or higher"
          when Whenever.seconds(1, :minute)...Whenever.seconds(1, :hour)
            minute_frequency = @time / 60
            timing[0] = comma_separated_timing(minute_frequency, 59, @at || 0)
          when Whenever.seconds(1, :hour)...Whenever.seconds(1, :day)
            hour_frequency = (@time / 60 / 60).round
            timing[0] = @at.is_a?(Time) ? @at.min : range_or_integer(@at, 0..59, 'Minute')
            timing[1] = comma_separated_timing(hour_frequency, 23)
          when Whenever.seconds(1, :day)...Whenever.seconds(1, :month)
            day_frequency = (@time / 24 / 60 / 60).round
            timing[0] = @at.is_a?(Time) ? @at.min  : 0
            timing[1] = @at.is_a?(Time) ? @at.hour : range_or_integer(@at, 0..23, 'Hour')
            timing[2] = comma_separated_timing(day_frequency, 31, 1)
          when Whenever.seconds(1, :month)...Whenever.seconds(1, :year)
            month_frequency = (@time / 30 / 24 / 60 / 60).round
            timing[0] = @at.is_a?(Time) ? @at.min  : 0
            timing[1] = @at.is_a?(Time) ? @at.hour : 0
            timing[2] = if @at.is_a?(Time)
              day_given? ? @at.day : 1

View on GitHub (pinned to 756163ed1a)

Solutions

  1. Raise the interval to at least one minute: every 1.minute.
  2. If sub-minute execution is required, move the job out of cron: a daemon/loop, Sidekiq or Resque scheduled jobs, or a wrapper script that loops with sleep and is itself triggered by a minutely cron entry.
  3. Run the job every minute and throttle inside the task (no-op if the last run was fewer than 30 seconds ago).

Example fix

# before
every 30.seconds do
  runner 'Heartbeat.ping'
end

# after
every 1.minute do
  runner 'Heartbeat.ping'
end
Defensive patterns

Strategy: type-guard

Validate before calling

interval = 30
abort('cron intervals must be 60 seconds (1.minute) or higher') if interval.is_a?(Numeric) && interval < 60
every interval

Type guard

def cron_schedulable?(seconds)
  seconds.is_a?(Numeric) && seconds.to_i >= 60
end

Try / catch

begin
  Whenever.cron(file: 'config/schedule.rb')
rescue ArgumentError => e
  abort('invalid interval in schedule: ' + e.message)
end

Prevention

When it happens

Trigger: every 30.seconds or every 5.seconds in config/schedule.rb (ActiveSupport durations are converted with to_i in time_in_cron_syntax); a raw integer below 60 such as every 45, which is interpreted as seconds; a computed frequency like every [jobs_count, 59].min that can land below 60.

Common situations: Porting a script that polled every 30 seconds via a sleep loop or a background-job interval into cron; new users assuming the every DSL supports second-level granularity; tightening an interval during load testing and hitting cron's floor.

Related errors


AI-assisted analysis of javan/whenever@756163ed1a (2026-08-21). Data as JSON: /api/errors/45b8668d0c037381. Report an issue: GitHub.