Background jobs in the Postgres you have.

Hopwire is a job queue for Node that stores jobs in your existing database. No Redis, no broker, and a job only exists once the transaction that created it commits.

Commits with your data
Enqueue inside the same transaction as the order. If it rolls back, the job never ran.
Retries you can read
Exponential backoff, a dead-letter table, and every attempt kept with its error.
Plain SQL to inspect
Jobs are rows. Query them with the tools you already use.

Installation

Hopwire needs Node 20 or later and PostgreSQL 13 or later.

npm install hopwire

Quickstart

Four steps from an empty project to a job running in a worker. Each takes a minute or two.

  1. Create the tables

    Hopwire adds three tables in a hopwire schema. Run the migration once per database.

    Terminal
    DATABASE_URL=postgres://localhost/shop npx hopwire migrate
  2. Define a job

    A job is a name and an async function. The payload is stored as JSON, so keep it small: pass IDs, not whole records.

    jobs/send-receipt.ts
    import { defineJob } from "hopwire";
    import { mailer } from "../mailer";
    
    export const sendReceipt = defineJob("send-receipt", {
      queue: "emails",
      retries: 5,
      async run({ orderId }: { orderId: string }) {
        await mailer.receipt(orderId); // throw to retry
      },
    });
  3. Enqueue in a transaction

    Pass the transaction you already have. The job becomes visible to workers only when it commits.

    routes/checkout.ts
    await db.transaction(async (tx) => {
      const order = await tx.orders.create(cart);
      await sendReceipt.enqueue({ orderId: order.id }, { tx });
    });

    Rolled back means never sent. If the order insert fails, the transaction rolls back and the receipt job disappears with it. There is no window where a worker sees a job for an order that doesn't exist.

  4. Run a worker

    Start one or more workers next to your app. They lock jobs with SKIP LOCKED, so you can run as many as your database allows.

    Terminal
    npx hopwire work --queues default,emails --concurrency 10

Retries and failures

When run throws, Hopwire records the error and schedules the next attempt with exponential backoff and jitter. After the last retry the job moves to hopwire.dead, where it stays until you retry or discard it.

AttemptDelay before itNote
10 sFirst run
215 sMost network errors clear here
31 min
45 min
530 minThen moved to hopwire.dead

Scheduled jobs

Run a job later with runAt, or on a repeating schedule with a cron expression. Schedules are stored in the database, so only one worker fires each run.

jobs/schedules.ts
// once, in an hour
await remindCart.enqueue({ cartId }, { runAt: "+1h" });

// every weekday at 07:00, in the shop's time zone
schedule(nightlyReport, { cron: "0 7 * * 1-5", tz: "Asia/Jakarta" });

Configuration

Pass options to createHopwire(), or set the matching environment variable. Options win over the environment.

OptionTypeDefaultWhat it does
connectionstring | PoolDATABASE_URLWhere jobs are stored. Reuse your app's pool to share connections.
schemastring"hopwire"Postgres schema for the three Hopwire tables.
concurrencynumber5Jobs one worker runs at the same time.
pollIntervalms1000Fallback poll when LISTEN notifications are unavailable.
retentionduration"7d"How long finished jobs are kept before cleanup.
onFailurefunctionnoneCalled after the last retry fails, for alerts.

Command line

Every command reads DATABASE_URL unless you pass --connection.

hopwire migrate
Create or upgrade the tables. Safe to run on every deploy.
hopwire work
Start a worker. Stops cleanly on SIGTERM after the running jobs finish.
hopwire status
Counts per queue and state, and the oldest waiting job.
hopwire retry <id>
Move a dead job back to its queue. Use --all with a queue name for a batch.

Changelog

Every release, newest first. Hopwire follows semantic versioning; breaking changes only ship in a new major version.

  1. 2.4.0

    Latest
    • AddedTime zones for cron schedules with the tz option.
    • Addedhopwire status --json for health checks.
    • FixedA worker no longer holds a connection while it waits for LISTEN.
  2. 2.3.2

    • FixedRetry delays now include jitter on the first retry too.
    • FixedThe migration works when the schema name contains a dash.
  3. 2.3.0

    • AddedThe onFailure hook, called once after the last retry.
    • ChangedFinished jobs are kept for seven days by default, down from thirty.
  4. 2.0.0

    • BreakingNode 20 or later is required.
    • Changedenqueue takes the transaction as { tx } instead of a third argument.
Hopwire is a fictional developer tool used to demonstrate this template. Template by Inazda