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

Job Priorities

Not all jobs are equal

A password reset email should be sent immediately. A weekly sales report can wait. A payment retry is urgent. An analytics sync is not. Without priorities, all jobs wait in the same line — a flood of report-generation jobs delays time-sensitive password resets.

The priority column

The jobs table has a priority column (INTEGER, default 0). Higher numbers mean higher priority. The dequeue query already sorts by priority:

ORDER BY priority DESC, scheduled_at ASC

Higher-priority jobs are claimed first. Among jobs with the same priority, older jobs (earlier scheduled_at) go first — FIFO within each priority level.

Defining priority levels

// src/priorities.ts
export const PRIORITY = {
  LOW: 0, // Analytics, cleanup, reports
  NORMAL: 5, // Standard notifications, syncs
  HIGH: 10, // Payment processing, important emails
  CRITICAL: 20, // Password resets, security alerts
} as const;
// Usage
enqueue("send_password_reset", { userId, token }, { priority: PRIORITY.CRITICAL });
enqueue("send_order_confirmation", { orderId }, { priority: PRIORITY.HIGH });
enqueue("generate_daily_report", { date }, { priority: PRIORITY.LOW });
enqueue("sync_inventory", { productId }, { priority: PRIORITY.NORMAL });

How priority affects ordering

-- Queue state (priority DESC, scheduled_at ASC):
-- 1. password_reset    priority=20  scheduled_at=10:00:05  ← processed first
-- 2. payment_retry     priority=10  scheduled_at=10:00:01  ← second
-- 3. order_confirmation priority=10  scheduled_at=10:00:03  ← third (same priority, older first)
-- 4. analytics_sync    priority=0   scheduled_at=09:55:00  ← last (low priority)

The password reset runs first despite being enqueued last, because it has the highest priority. The analytics sync was enqueued 5 minutes earlier but runs last because of its low priority.

Starvation prevention

A constant stream of high-priority jobs could starve low-priority jobs — they never run because something higher-priority is always ahead. In practice, this is rare for most applications. If it happens, two strategies help:

Priority aging: Increase a job’s priority the longer it waits:

-- Bump priority for jobs waiting more than 10 minutes
UPDATE jobs
SET priority = priority + 1
WHERE status = 'pending'
  AND scheduled_at < datetime('now', '-10 minutes')
  AND priority < 20;

Dedicated workers: Run separate workers for different priority levels. One worker handles only CRITICAL and HIGH jobs. Another handles NORMAL and LOW. This guarantees low-priority jobs get processing time.

Exercises

Exercise 1: Enqueue 5 jobs with different priorities. Verify the worker processes them in priority order.

Exercise 2: Enqueue a LOW priority job, then a HIGH priority job. Verify the HIGH job runs first even though it was enqueued later.

Exercise 3: Implement priority aging. Enqueue a LOW priority job. Wait 10 minutes (or set the threshold to 10 seconds for testing). Verify its priority increases.

Why does the dequeue query sort by priority DESC then scheduled_at ASC?

← Concurrency and Locking Rate-Limiting Jobs →

© 2026 hectoday. All rights reserved.