improvement(schedules): jitter scheduled execution starts by 0-30s (#4750)

Cron schedules all fire on the same boundary (e.g. every :00), stampeding
the Postgres connection pool at the top of each minute/hour. Spread each due
schedule's start across a [0, 30s) window via trigger.dev's delay option
(no compute billed during the delay). Wires the previously-unused
EnqueueOptions.delayMs through the trigger.dev backend.
This commit is contained in:
Theodore Li
2026-05-26 21:57:35 -04:00
committed by GitHub
parent 0fedceb4c3
commit a1aa168cfe
2 changed files with 12 additions and 0 deletions
@@ -26,6 +26,14 @@ const JOB_CHUNK_SIZE = 100
const MAX_TICK_DURATION_MS = 3 * 60 * 1000
const STALE_SCHEDULE_CLAIM_MS = getMaxExecutionTimeout()
/**
* Upper bound (ms) for the random start delay applied to each scheduled
* execution. Cron schedules all fire on the same boundary (e.g. every `:00`),
* which stampedes the database connection pool at the top of each minute/hour.
* Spreading starts across a [0, 30s) window smooths that burst.
*/
const SCHEDULE_JITTER_MAX_MS = 30_000
const dueFilter = (queuedAt: Date) =>
and(
isNull(workflowSchedule.archivedAt),
@@ -217,6 +225,7 @@ async function processScheduleItem(
const jobId = await jobQueue.enqueue('schedule-execution', payload, {
jobId: scheduleJobId,
concurrencyKey: scheduleJobId,
delayMs: Math.floor(Math.random() * SCHEDULE_JITTER_MAX_MS),
metadata: {
workflowId: schedule.workflowId ?? undefined,
workspaceId: resolvedWorkspaceId ?? undefined,
@@ -81,6 +81,9 @@ export class TriggerDevJobQueue implements JobQueueBackend {
triggerOptions.idempotencyKey = options.jobId
triggerOptions.idempotencyKeyTTL = '14d'
}
if (options?.delayMs && options.delayMs > 0) {
triggerOptions.delay = new Date(Date.now() + options.delayMs)
}
const handle = await tasks.trigger(taskId, enrichedPayload, triggerOptions)
logger.debug('Enqueued job via trigger.dev', { jobId: handle.id, type, taskId, tags })