hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Background Jobs and Queues with @hectoday/http

Why Background Jobs

  • The Request Cycle Problem
  • Project Setup

Building a Queue

  • Database-Backed Queues
  • The Worker Loop
  • Job Serialization

Reliability

  • Retries and Backoff
  • Dead Letter Queues
  • Idempotent Jobs
  • Job Timeouts and Stale Jobs

Scheduling

  • Delayed Jobs
  • Recurring Jobs (Cron)

Scaling

  • Concurrency and Locking
  • Job Priorities
  • Rate-Limiting Jobs

Patterns

  • Job Chaining and Workflows
  • Monitoring and Observability

Putting It All Together

  • Capstone: Order Processing Pipeline

Delayed Jobs

Jobs that should not run immediately

Some jobs need to wait. “Send a follow-up email 24 hours after the order.” “Expire the password reset token in 1 hour.” “Retry the payment in 30 minutes.” These are delayed jobs — they sit in the queue until their scheduled time arrives.

The scheduled_at column

The jobs table already has a scheduled_at column. The dequeue query already checks it:

WHERE status = 'pending' AND scheduled_at <= datetime('now')

A job with scheduled_at in the future is invisible to the worker — it will not be picked up until the time arrives. This is all that delayed jobs need. No new tables, no new queries.

Enqueueing delayed jobs

Use the scheduledAt option in enqueue:

// Send a follow-up email in 24 hours
enqueue(
  "send_followup_email",
  { orderId: order.id },
  {
    scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
  },
);

// Expire a password reset token in 1 hour
enqueue(
  "expire_reset_token",
  { tokenId: token.id },
  {
    scheduledAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
  },
);

// Retry a payment in 30 minutes
enqueue(
  "retry_payment",
  { orderId: order.id },
  {
    scheduledAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
  },
);

The job is inserted with status "pending" and a future scheduled_at. The worker polls every second, but this job will not appear in the dequeue results until the scheduled time.

A helper for delays

export function enqueueDelayed(
  type: string,
  payload: Record<string, unknown>,
  delayMs: number,
  options: { priority?: number; maxAttempts?: number } = {},
): string {
  return enqueue(type, payload, {
    ...options,
    scheduledAt: new Date(Date.now() + delayMs).toISOString(),
  });
}

// Usage
enqueueDelayed("send_followup_email", { orderId: order.id }, 24 * 60 * 60 * 1000);
enqueueDelayed("expire_reset_token", { tokenId: token.id }, 60 * 60 * 1000);

How retries create delayed jobs

The Retries and Backoff lesson already uses delayed jobs. When a job fails and has retries remaining, failJob sets scheduled_at to a future time:

scheduled_at = datetime('now', '+' || (attempts + 1) * 30 || ' seconds')

A failed job with 2 attempts retries in 90 seconds. It is a pending job with a future scheduled_at — the same mechanism as explicitly delayed jobs. The queue has one concept (scheduled_at) that serves two purposes (intentional delays and retry backoff).

Canceling delayed jobs

A user cancels their order. The follow-up email should not be sent:

export function cancelJob(jobId: string): boolean {
  const result = db.prepare("DELETE FROM jobs WHERE id = ? AND status = 'pending'").run(jobId);

  return result.changes > 0;
}

Store the job ID when enqueueing so you can cancel later:

const followupJobId = enqueueDelayed("send_followup_email", { orderId: order.id }, 86400000);
// Store followupJobId on the order for cancellation
db.prepare("UPDATE orders SET followup_job_id = ? WHERE id = ?").run(followupJobId, order.id);

Exercises

Exercise 1: Enqueue a delayed job with a 10-second delay. Verify the worker does not pick it up immediately. Wait 10 seconds. Verify it runs.

Exercise 2: Enqueue a delayed job and then cancel it before it runs. Verify it is deleted.

Exercise 3: Query all scheduled (future) jobs: SELECT * FROM jobs WHERE scheduled_at > datetime('now'). Verify your delayed jobs appear.

How does the queue know not to process a delayed job before its scheduled time?

← Job Timeouts and Stale Jobs Recurring Jobs (Cron) →

© 2026 hectoday. All rights reserved.