HalyardDocumentation

Retries and failure

Backoff, dead-letter handling, and how to make retries safe.

Default behaviour

A job that throws is retried with exponential backoff: 1s, 2s, 4s, 8s, and so on, up to 5 attempts. After the final attempt it moves to the dead-letter state and stops.

Configure per job:

export const chargeCard = defineJob({
  name: "charge-card",
  retries: { attempts: 3, backoff: "exponential", base: "2s", jitter: true },
  async handler({ chargeId }) { /* ... */ },
});

Always use jitter

jitter: true is the default and you should leave it on.

Without jitter, every client that failed at the same moment retries at the same moment. A brief downstream blip produces a synchronised retry wave that is often worse than the original failure — the retries themselves become the outage.

Jitter spreads retries across the backoff window, which desynchronises clients that failed together.

At-least-once delivery

Halyard guarantees at least once, not exactly once. A job can run twice if a worker dies after completing the work but before marking the row done.

This means handlers must be idempotent. In practice:

async handler({ chargeId }) {
  const charge = await charges.find(chargeId);
  if (charge.status === "captured") return;   // already done, safely exit
  await paymentProvider.capture(charge.token, { idempotencyKey: chargeId });
  await charges.markCaptured(chargeId);
}

The guard at the top and the provider’s own idempotency key both matter. The guard handles the common case cheaply; the key handles the race where two workers pass the guard simultaneously.

Non-retryable failures

Some failures will never succeed on retry — malformed input, a deleted record, a permanently rejected card. Throw PermanentError to skip straight to dead-letter:

import { PermanentError } from "halyard";

if (!order) throw new PermanentError(`order ${orderId} no longer exists`);

Retrying these wastes capacity and delays the jobs behind them.

Replaying dead letters

npx halyard dead-letter list
npx halyard dead-letter replay --id 01J2X...
npx halyard dead-letter replay --job send-receipt --since 2h

Replay resets the attempt counter. Fix the cause before replaying in bulk, or you will simply refill the dead-letter queue.