Skip to main content
The DeployStack backend includes a custom background job queue system for processing long-running tasks that cannot be completed within a typical HTTP request/response cycle. The system uses database-backed persistence with automatic retries and rate limiting.

Overview

The job queue system solves the challenge of processing tasks that require:
  • Extended execution time - Operations taking minutes to hours
  • External API dependencies - Calls to third-party services with rate limits
  • Large-scale batch operations - Processing hundreds or thousands of items
  • Failure resilience - Automatic retry logic for transient errors
  • Progress tracking - Monitor task completion status

Architecture

The system consists of four core components working together:

JobQueueService

CRUD operations for managing jobs in the database. Handles job creation, status updates, and retrieval of pending jobs.

JobProcessorService

Background worker loop that polls the database every second, processes jobs sequentially, respects rate limits via scheduled_for timestamps, and implements exponential backoff for retries.

Worker Registry

Plugin-style pattern where each worker implements the Worker interface. Workers are registered by type (e.g., send_email, process_csv, sync_registry) and execute specific job types.

Database Tables

Two tables provide persistence: queueJobs stores individual jobs with payload, status, and result data, while queueJobBatches tracks groups of related jobs for progress monitoring.

Core Concepts

Jobs

Individual units of work with a specific type, JSON payload, and status tracking. Jobs flow through states: pendingprocessingcompleted or failed.

Workers

Execution handlers that implement the Worker interface. Each worker is responsible for one job type and receives payload data plus context (database, logger).

Batches

Logical grouping of related jobs that enables tracking progress across multiple jobs. Useful for operations like sending 1,000 emails or processing a large CSV file.

Rate Limiting

Built-in support through the scheduled_for field. Jobs scheduled for future execution remain in pending state until their scheduled time, preventing API rate limit violations.

When to Use Job Queue vs Events

The job queue system complements rather than replaces the Global Event Bus.
Use Job Queue for:
  • Long-running operations (>30 seconds)
  • Tasks requiring retry logic
  • Operations that must survive server restarts
  • Batch processing with progress tracking
  • Rate-limited external API calls
Use Event Bus for:
  • Fire-and-forget notifications
  • Real-time event distribution
  • Triggering multiple actions from one event
  • Low-latency intra-application messaging
Combining Both: Event listeners can create jobs for heavy processing:

Creating Workers

Workers live in services/backend/src/workers/ and implement the Worker interface from workers/types.ts.

Worker Interface

The data field in WorkerResult is automatically stored in the database when a job completes successfully. This allows you to inspect job results through the admin UI or database queries.

Basic Worker Pattern

Worker Registration

Register workers in workers/index.ts:

Error Handling Strategies

Retriable Errors - Throw errors for temporary failures that might succeed on retry:
Non-Retriable Errors - Return failure for permanent errors that won’t be fixed by retrying:

Worker Best Practices

  1. Keep Workers Stateless - No state between executions
  2. Inject Dependencies - Database, logger, and services through constructor
  3. Validate Payloads - Always validate before processing
  4. Use Structured Logging - Include context objects with operation, jobId, and relevant data
  5. Single Responsibility - One worker per job type
  6. Make Testable - Design for easy unit testing with mocked dependencies

Creating Jobs

From API Routes

With Rate Limiting

Batch Operations

Server Integration

The job queue integrates with the backend server lifecycle in server.ts within the initializeDatabaseDependentServices function.

Initialization

The job queue system is initialized automatically after the database is ready:

Graceful Shutdown

Graceful shutdown is handled in the onClose hook to ensure current jobs complete before server shutdown:
The job processor’s stop() method will:
  1. Stop accepting new jobs from the queue
  2. Wait for the current job to complete (with 30-second timeout)
  3. Clean up resources
This ensures no jobs are interrupted mid-execution during server shutdown.

Job Monitoring

Job Status Lifecycle

Jobs transition through these states:
  1. pending - Job created, waiting to be processed
  2. processing - Currently executing
  3. completed - Executed successfully
  4. failed - Execution failed after all retries

Database Queries

Check job status and result:
Monitor batch progress:

Common Issues

Jobs Not Processing:
  • Verify JobProcessorService is started
  • Check worker is registered for job type
  • Review server logs for errors
Jobs Stuck in Processing:
  • Server crashed during job execution
  • Manual intervention: Set status back to pending
  • System will retry after exponential backoff
High Retry Count:
  • Worker throwing errors unnecessarily
  • External service temporarily unavailable
  • Review worker error handling logic

Database Schema

For the complete database schema, see schema.ts in the backend directory.

Jobs Table

Batches Table

System Behavior

Processing Loop

  1. Poll database every 1 second for pending jobs
  2. Check if job’s scheduled_for time has passed
  3. Lock job by setting status to processing
  4. Execute worker for job type
  5. On success: store worker’s data in result column, set status to completed
  6. On failure: implement exponential backoff for retries (1s, 2s, 4s, etc.)

Resource Usage

  • CPU: <5% during normal operation (mostly waiting)
  • Memory: Minimal footprint, jobs processed sequentially
  • Database: Single query per second, additional writes during processing
  • Non-blocking: Async processing doesn’t block main event loop

Performance Characteristics

  • Throughput: 1-60 jobs per minute depending on job duration
  • Latency: 1-second maximum delay before job starts
  • Concurrency: Sequential processing prevents resource overload
  • Scalability: Suitable for small to medium deployments

Design Decisions

Why Database-Backed?

No additional infrastructure required (Redis, message queues). Uses existing PostgreSQL database, and jobs persist across server restarts.

Why Sequential Processing?

Prevents server resource exhaustion from hundreds of concurrent operations. Simplifies rate limiting for external APIs. Adequate for most use cases (1,000 items = 15-30 minutes).

Why 1-Second Polling?

Balance between responsiveness and database load. Adequate latency for background tasks. Can be adjusted if needed.

Limitations

  • Not suitable for sub-second latency requirements
  • Single-server deployment (no distributed workers)
  • Sequential processing limits throughput
For recurring scheduled tasks, see the Cron Job Scheduling documentation which integrates with this job queue system.

Migration Path

If scaling beyond single-server becomes necessary, clear upgrade paths exist:
  • Redis Backend: Migrate to BullMQ for distributed processing
  • PostgreSQL: Switch to pg-boss or Graphile Worker
  • Cloud Queues: Move to AWS SQS, Google Cloud Tasks, etc.
Worker interface remains compatible, simplifying migration.

Common Use Cases

Batch Email Sending

Send emails to hundreds of users with rate limiting to avoid SMTP throttling.

CSV File Processing

Process uploaded files row-by-row, validate data, and store results.

External API Synchronization

Fetch data from third-party APIs respecting rate limits (e.g., 1 request per second).

Database Backups

Schedule periodic database backups and upload to cloud storage.

Report Generation

Generate complex reports from large datasets without blocking API requests.

Summary

The background job queue system provides a simple, reliable way to process long-running tasks in DeployStack. Built on PostgreSQL infrastructure, it requires no additional services while providing persistence, retry logic, and rate limiting. Workers follow a straightforward pattern making them easy to implement and test. For routine operations, the system handles thousands of jobs efficiently. For specialized needs requiring higher throughput or distributed processing, the architecture supports clear migration paths to more advanced solutions.