Skip to main content
Job Schedulers support several options to control various aspects of job repetitions. These options work with all repeat strategies (every, cron, and custom).

Available Options

startDate

Sets a future date from which the job will start being scheduled. This is useful for setting up jobs that should begin repeating on a specific day.
import { Queue } from 'bullmq';

const connection = { host: 'localhost', port: 6379 };
const myQueue = new Queue('my-dated-jobs', { connection });

await myQueue.upsertJobScheduler(
  'start-later-job',
  {
    every: 60000, // every minute
    startDate: new Date('2024-10-15T00:00:00Z'), // start on October 15, 2024
  },
  {
    name: 'timed-start-job',
    data: { message: 'Starting later' },
  },
);
startDate
Date | string | number
Timestamp when the repeat should start. Can be a Date object, ISO string, or milliseconds.
Jobs will not be scheduled before the startDate. If you create a scheduler with a startDate in the future, the first job will be created at that time.

endDate

Specifies when the job should stop being scheduled, effectively setting an expiration date for the job repetitions.
await myQueue.upsertJobScheduler(
  'end-soon-job',
  {
    every: 60000, // every minute
    endDate: new Date('2024-11-01T00:00:00Z'), // end on November 1, 2024
  },
  {
    name: 'timed-end-job',
    data: { message: 'Ending soon' },
  },
);
endDate
Date | string | number
Timestamp when the repeat should end. Can be a Date object, ISO string, or milliseconds.
Once the endDate is reached, no new jobs will be created. The scheduler will automatically stop producing jobs.

limit

Limits the number of times a job will be repeated. When the count reaches this limit, no more jobs will be produced for the given job scheduler.
await myQueue.upsertJobScheduler(
  'limited-job',
  {
    every: 10000, // every 10 seconds
    limit: 10, // limit to 10 executions
  },
  {
    name: 'limited-execution-job',
    data: { message: 'Limited runs' },
  },
);
limit
number
Maximum number of times the job should repeat. After reaching this count, the scheduler stops.

immediately (Deprecated)

From version 5.19.0 onwards, the “immediately” option has been deprecated. The current behavior is as if “immediately” was always true.
Previously, this setting forced the job to execute as soon as it was added, regardless of the schedule.

Historical Context

When using the every option, jobs are scheduled based on fixed time intervals aligned with the clock. For instance, with an interval of 2000ms, jobs trigger at every even second—0, 2, 4, 6, 8 seconds, and so on. Before v5.19.0, if you wanted a job to begin processing immediately after adding a job scheduler (rather than waiting for the next aligned interval), you would use immediately: true.

Current Behavior (v5.19.0+)

Now, the first repetition always executes immediately when you create a new job scheduler, then repeats according to the every or pattern setting. Subsequent calls to upsertJobScheduler for existing schedulers will not lead to immediate repetitions and will instead follow the configured interval.
// First call: Job executes immediately, then every day
await myQueue.upsertJobScheduler(
  'immediate-job',
  {
    every: 86400000, // once a day
  },
  {
    name: 'instant-job',
    data: { message: 'Immediate start' },
  },
);

// Subsequent update: No immediate execution, follows existing schedule
await myQueue.upsertJobScheduler(
  'immediate-job',
  {
    every: 86400000,
  },
  {
    name: 'instant-job',
    data: { message: 'Updated data' },
  },
);

tz (Timezone)

Specifies the timezone for cron pattern scheduling. This is crucial for jobs that need to run at specific local times.
await myQueue.upsertJobScheduler(
  'daily-est',
  {
    pattern: '0 9 * * *', // 9 AM
    tz: 'America/New_York',
  },
  {
    name: 'morning-report',
    data: {},
  },
);
tz
string
IANA timezone identifier (e.g., ‘America/New_York’, ‘Europe/London’, ‘Asia/Tokyo’)
Timezone support handles daylight saving time transitions automatically. Your jobs will continue to run at the correct local time regardless of DST changes.

Common Timezones

// North America
'America/New_York'    // Eastern Time
'America/Chicago'     // Central Time
'America/Denver'      // Mountain Time
'America/Los_Angeles' // Pacific Time

// Europe
'Europe/London'       // GMT/BST
'Europe/Paris'        // CET/CEST
'Europe/Berlin'       // CET/CEST

// Asia
'Asia/Tokyo'          // JST
'Asia/Shanghai'       // CST
'Asia/Dubai'          // GST

// Other
'Australia/Sydney'    // AEST/AEDT
'UTC'                 // Coordinated Universal Time

offset

Applies an offset in milliseconds to the calculated next run time. This is useful for staggering job execution or adding small delays.
await myQueue.upsertJobScheduler(
  'offset-job',
  {
    every: 60000, // every minute
    offset: 5000, // add 5 seconds delay
  },
  {
    name: 'delayed-task',
    data: {},
  },
);
offset
number
Offset in milliseconds to apply to the next iteration time. Can be positive (delay) or negative (advance).

Combining Options

You can combine multiple options to create sophisticated scheduling patterns:

Example: Limited Campaign

// Run daily reports for 30 days
await myQueue.upsertJobScheduler(
  'month-campaign',
  {
    pattern: '0 9 * * *',      // Every day at 9 AM
    tz: 'America/New_York',     // Eastern Time
    startDate: new Date('2024-01-01'),
    endDate: new Date('2024-01-31'),
    limit: 30,                  // Maximum 30 executions
  },
  {
    name: 'daily-campaign-report',
    data: { campaignId: 'jan-2024' },
  },
);

Example: Scheduled Maintenance

// Weekly maintenance starting next month
await myQueue.upsertJobScheduler(
  'weekly-maintenance',
  {
    pattern: '0 2 * * 0',      // Sundays at 2 AM
    tz: 'UTC',
    startDate: new Date('2024-02-01'),
    offset: 300000,             // 5-minute delay
  },
  {
    name: 'system-maintenance',
    data: { type: 'full' },
    opts: {
      attempts: 3,
      backoff: {
        type: 'exponential',
        delay: 60000,
      },
    },
  },
);

Example: Trial Period

// Send reminders every 3 days for 30 days
await myQueue.upsertJobScheduler(
  'trial-reminders',
  {
    every: 3 * 24 * 60 * 60 * 1000, // Every 3 days
    startDate: new Date(),           // Start now
    limit: 10,                       // 10 reminders total
  },
  {
    name: 'trial-reminder',
    data: { userId: 'user123' },
  },
);

Complete Options Reference

pattern
string
Cron expression for scheduling
every
number
Interval in milliseconds between executions
limit
number
Maximum number of times to repeat
startDate
Date | string | number
When to start scheduling
endDate
Date | string | number
When to stop scheduling
tz
string
IANA timezone identifier for cron patterns
offset
number
Offset in milliseconds to apply to next run time
immediately
boolean
deprecated
Deprecated: First execution is now always immediate for new schedulers

Validation Rules

BullMQ enforces several validation rules when creating job schedulers:
  1. Cannot use both pattern and every
    // ❌ Invalid
    await queue.upsertJobScheduler('id', {
      pattern: '0 * * * *',
      every: 60000,
    });
    
  2. Must specify either pattern or every
    // ❌ Invalid
    await queue.upsertJobScheduler('id', {});
    
  3. Cannot use both immediately and startDate (pre-v5.19.0)
    // ❌ Invalid
    await queue.upsertJobScheduler('id', {
      every: 60000,
      immediately: true,
      startDate: new Date('2024-01-01'),
    });
    

Best Practices

1

Always specify timezone for cron patterns

Avoid ambiguity and DST issues:
await queue.upsertJobScheduler('id', {
  pattern: '0 9 * * *',
  tz: 'America/New_York', // Clear and explicit
});
2

Use endDate or limit for temporary schedulers

Prevent forgotten schedulers from running indefinitely:
await queue.upsertJobScheduler('id', {
  every: 60000,
  endDate: new Date('2024-12-31'), // Auto-cleanup
});
3

Consider offset for load distribution

Stagger jobs to avoid resource spikes:
await queue.upsertJobScheduler('scheduler-1', {
  every: 60000,
  offset: 0,
});

await queue.upsertJobScheduler('scheduler-2', {
  every: 60000,
  offset: 20000, // 20 seconds later
});
4

Test with startDate in development

Validate scheduling logic without waiting:
await queue.upsertJobScheduler('test-scheduler', {
  pattern: '0 9 * * *',
  startDate: new Date(Date.now() + 5000), // 5 seconds from now
  limit: 3, // Only run 3 times
});

Repeat Strategies

Learn about every, cron, and custom strategies

Overview

Understand Job Schedulers basics

Management

Manage and monitor schedulers

RepeatableOptions

Full API reference

API Reference

Build docs developers (and LLMs) love