Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
Queue Providers
Hedhog processes background jobs — email sending, report generation, file processing, scheduled tasks — through @hed-hog-pro/queue. Only the Database provider is actually implemented today. RabbitMQ and BullMQ exist as settings and a provider enum value, but there is no working RabbitMQ or BullMQ worker behind them yet — picking either one in settings will not move jobs.
This isn't a configuration mistake on your part if it doesn't work; it's the current state of the library. Build on Database, and treat the RabbitMQ/BullMQ settings as reserved for a future release rather than something to debug.
Status: ⚠️ Partial — Database provider is fully implemented; RabbitMQ and BullMQ are reserved settings with no provider behind them yet.

Quick Setup
- Leave
queue.providerset todatabase— it's the only one that works - Register a job handler via
QueueModule.register({ handlers: {...} }) - Enqueue a job and confirm a
queue_jobrow appears, then flips frompendingtodone
How It Works (Database provider)
- Application code enqueues a job, e.g.
QueueService.add('send-email', payload) - The job is written as a row in the
queue_jobtable withstatus = 'pending' QueueWorkerProcessorpolls that table on an interval, atomically claims a batch of jobs via an optimistic-lockupdateMany(so multiple API instances can run workers without double-processing the same job), and invokes the matching registered handler- On success the job is marked done; on failure it's retried up to
queue.default_max_attempts, then marked failed - Jobs stuck in
processingpastqueue.database.lock_timeout_seconds(e.g. a worker crashed mid-job) are automatically released back for retry — you don't need to manually unstick them
Real Setting Keys
Unlike most other recipes, queue settings use dot-separated keys, not hyphens — don't second-guess yourself if you see queue.database.poll_interval_seconds instead of queue-database-poll-interval-seconds, that's correct.
Provider selection & defaults
| Setting key | Description |
|---|---|
queue.provider | Active provider — leave this as database, the only one that works |
queue.default_max_attempts | Default retries before a job is marked failed (default 3) |
queue.default_retry_delay_seconds | Delay before retrying a failed job (default 300) |
queue.default_timeout_seconds | Max time a single job run gets before being considered stuck (default 900) |
Database provider (working)
| Setting key | Description |
|---|---|
queue.database.enabled | Enable the DB-backed provider (default true) |
queue.database.poll_interval_seconds | How often the worker checks for new jobs (default 5) |
queue.database.batch_size | Jobs claimed per poll (default 20) |
queue.database.lock_timeout_seconds | How long a job can stay processing before being treated as stale and released (default 300) |
Worker behavior (applies regardless of provider)
| Setting key | Description |
|---|---|
queue.worker.enabled | Whether this instance runs a worker at all — set to false on instances that should only enqueue, never process |
queue.worker.heartbeat_interval_seconds | How often the worker reports liveness (default 30) |
queue.worker.dynamic_scope_enabled | Allow the set of queues this worker processes to change at runtime without a restart (default true) |
queue.worker.scope_refresh_interval_seconds | How often the worker re-checks its assigned queue scope (default 5) |
queue.worker.scope_drain_timeout_seconds | Grace period to finish in-flight jobs when a queue is removed from scope (default 30) |
RabbitMQ / BullMQ (reserved, not implemented)
| Setting key | Status |
|---|---|
queue.rabbitmq.enabled, queue.rabbitmq.url, queue.rabbitmq.exchange, queue.rabbitmq.default_queue | Settings exist; no consuming code exists yet |
queue.bullmq.enabled, queue.bullmq.redis_url | Settings exist; no consuming code exists yet |
Don't spin up RabbitMQ or Redis expecting Hedhog to pick them up — there's nothing on the other end listening yet.
Defining Job Handlers
Register handlers in your NestJS module:
import { QueueModule } from '@hed-hog-pro/queue';
@Module({
imports: [
QueueModule.register({
handlers: {
'send-email': SendEmailHandler,
'process-report': ProcessReportHandler,
},
}),
],
})
export class AppModule {}
Each handler is a class with a handle(job) method:
@Injectable()
export class SendEmailHandler implements JobHandler {
async handle(job: Job<{ to: string; subject: string; body: string }>) {
await this.mail.send(job.data);
}
}
Throw a NonRetryableError (from @hed-hog-pro/queue) instead of a regular error if a failure should not be retried — for example, a job that failed because the input data was permanently invalid, not because of a transient outage.
Scaling the Database Provider
Since the worker uses an atomic claim (updateMany with a status filter as an optimistic lock), it's safe to run multiple API instances with queue.worker.enabled = true simultaneously — they won't double-process the same job. Throughput is bounded by queue.database.poll_interval_seconds and queue.database.batch_size; lower the poll interval or raise the batch size for higher volume, at the cost of more frequent polling load on PostgreSQL.
Verify It Worked
- Enqueue a test job and confirm a row appears in
queue_jobwithstatus = 'pending', then flips todonewithinqueue.database.poll_interval_seconds - Force a handler to throw once and confirm the job retries instead of disappearing, up to
queue.default_max_attempts
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Jobs enqueue but never run | queue.worker.enabled is false on every running instance |
queue.provider set to rabbitmq or bullmq and nothing happens | Expected — neither provider is implemented; switch back to database |
A job is stuck processing indefinitely | The worker that claimed it crashed before queue.database.lock_timeout_seconds elapsed — it will self-release after that timeout, not before |
| Same job processed twice | Should not happen with the Database provider's optimistic lock — if you see this, check for a custom handler that isn't idempotent rather than the queue layer itself |