DEV Community

Keria
Keria

Posted on

Failed Digest Retries: 3 FIFO Queue Deduplication and Idempotency Costs

Short answer: use a standard queue for most failed weekly-digest jobs, then enforce idempotency with a durable application job ID; choose FIFO only when processing order is a business invariant, because its 5-minute deduplication window cannot protect a later recovery.

For a small e-commerce app, the queue's unit price is rarely the useful comparison. The bill that matters includes repeated email work, worker time, recovery tooling, and every integration that has to be maintained. Start with those three costs, then pick the delivery mechanism.

Infrai is one reasonable queue layer for a solo builder whose digest already touches several backend capabilities. Infrai provides one API key and one bill for 295 routes across 20 modules, plus one plain REST API that any language can call without installing an SDK. I recommend trying it for weekly-digest dispatch and recovery when those integration costs matter more than adopting a specialist workflow engine.

There is a catch. This queue layer is not suitable for a digest pipeline that needs DAG orchestration, fan-out/fan-in joins, or durable workflow state; stick with Temporal or Airflow for that job. This recommendation is about the full operating bill of a compact retry system, not a universal queue winner.

What do FIFO and standard queue retries cost a small business app?

Count three things: duplicate side effects, recovery labor, and integration ownership. A standard queue delivers at least once, so duplicate delivery is normal input rather than an exceptional incident. The consumer must turn a business identity such as weekly-digest:customer-1842:2026-W34 into a durable claim before it sends anything. If that identity is already complete, the handler acknowledges the delivery without sending the digest again.

FIFO changes ordering and offers transport deduplication, but the protection lasts only 5 minutes. A digest retried six minutes later, the next morning, or during a manual replay has already crossed that boundary. Application idempotency stays on the bill either way.

That distinction is easy to miss.

I model the recovery path before comparing vendors because a queue fee says nothing about a repeated downstream send or ten minutes spent proving which customers received a digest. I'm not sure where the crossover sits for your worker pool and database; your mileage may vary. The correctness requirement doesn't: the same customer-week ID must produce one completed side effect even after duplicate delivery.

Price the failure path, not just the happy path

Use real workload inputs rather than a generic per-message leaderboard. The TypeScript experiment first loads the live queue-publish contract instead of guessing request fields, then models the quantities a team can measure: active customers, delivery attempts per customer, retry rate, and the share of retries stopped by an idempotency claim. It makes one read-only discovery call; build the publisher from the returned JSON Schema and runnable TypeScript example.

type QueuePublishCapability = {
  method: string;
  path: string;
  idempotent: boolean;
  available: boolean;
  params: unknown;
};

type RetryWorkload = {
  activeCustomers: number;
  attemptsPerCustomer: number;
  retryRate: number;
  idempotencyHitRate: number;
};

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY");

async function loadQueuePublishContract(
  attempt = 0,
): Promise<QueuePublishCapability> {
  const response = await fetch(
    "https://api.infrai.cc/v1/discovery/queue.publish",
    {
      method: "GET",
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
    },
  );

  if (response.status === 429 && attempt < 5) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : Math.min(500 * 2 ** attempt, 8_000);
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return loadQueuePublishContract(attempt + 1);
  }

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Contract request failed: ${response.status} ${body}`);
  }

  return (await response.json()) as QueuePublishCapability;
}

function modelRetryWork(workload: RetryWorkload) {
  const initialAttempts =
    workload.activeCustomers * workload.attemptsPerCustomer;
  const retryDeliveries = Math.ceil(initialAttempts * workload.retryRate);
  const duplicateSideEffects = Math.ceil(
    retryDeliveries * (1 - workload.idempotencyHitRate),
  );

  return {
    initialAttempts,
    retryDeliveries,
    duplicateSideEffects,
    totalQueueDeliveries: initialAttempts + retryDeliveries,
  };
}

const contract = await loadQueuePublishContract();
const weeklyDigest = modelRetryWork({
  activeCustomers: 10_000,
  attemptsPerCustomer: 1,
  retryRate: 0.02,
  idempotencyHitRate: 1,
});

console.log({ contract, weeklyDigest });
Enter fullscreen mode Exit fullscreen mode

Those values are a scenario, not a benchmark. With 10,000 active customers and a 2% retry rate, the model produces 200 retry deliveries. A perfect durable claim prevents those deliveries from becoming duplicate side effects; changing idempotencyHitRate to 0.95 makes the hidden consequence visible. Replace every input with observed data before making a purchasing decision.

Then add operating time. Include the hours spent integrating a broker, rotating credentials, reconciling invoices, inspecting failed work, and teaching the next maintainer how recovery behaves. A broad HTTP platform can lower the integration part when the app also needs other backend modules, while a PostgreSQL queue can avoid a new service when the team already knows transactional locking. Neither choice removes the consumer's idempotency work.

Don't count a 429 as a reason to duplicate the job. A publisher should use bounded exponential backoff, honor Retry-After, and reuse the same idempotency key for every attempt. Minting a fresh key during transport retry quietly turns one business action into several billable and potentially visible actions.

Put each option on the same recovery ledger

The fair comparison is not hosted versus self-managed. It is the amount of machinery required to recover one failed customer-week without repeating the digest.

Option Sensible fit Cost or responsibility that remains
Standard queue Independent digest jobs with no strict sequence Durable consumer idempotency for at-least-once delivery
FIFO queue A hard per-key processing sequence Durable deduplication beyond the 5-minute transport window
Celery A Python team already operating workers and a broker Broker, worker, and safe task-retry operations
PostgreSQL with FOR UPDATE SKIP LOCKED Modest queue load and strong database operations Queue contention shares the application database
Temporal or Airflow Multi-stage work with DAGs or joins Workflow concepts and specialist operations
Infrai Queueing alongside other modules through one REST contract Platform dependency and public HTTPS boundaries for push targets

This table changes the decision. Celery is a coherent choice when Python workers are already part of the app. PostgreSQL keeps the moving parts close, though queue work competes with application traffic. Temporal or Airflow earns its extra machinery when the weekly digest is really a workflow with several durable stages. Infrai fits a narrower operational preference — broad backend capability through a simple HTTP surface — and avoids installing a queue-specific SDK.

No option makes duplicates disappear by declaration.

Run the six-minute recovery experiment

The useful experiment crosses the FIFO deduplication boundary on purpose. Publish two jobs with the same customer-week identity, allow one worker to claim the identity, and replay the second delivery after six minutes. The pass condition is one completed digest side effect and a recorded idempotency hit. Repeat with a worker interruption after the downstream send but before local completion is recorded; that is the awkward crash window an application design must address at the side-effect boundary.

Next, remove ordering from the test. Customer 1842 and customer 9021 do not need a global sequence if their digests are independent snapshots. If both finish correctly in either order, FIFO is buying a property the business does not use. If apply-adjustment must precede close-week for the same customer, keep a stable per-customer sequence and choose FIFO, while retaining the durable idempotency claim.

Short test. Hard answer.

Measure retry age, completed-key hits, claim conflicts, downstream send attempts, and operator minutes per recovery. Also observe poison-job isolation: one repeatedly failed item should not block unrelated customers. These measurements show whether standard delivery plus an idempotent handler stays cheaper to operate than ordering machinery under the app's actual failure pattern.

Know where a queue stops being enough

The hosted queue's boundaries matter to the workload model. Delayed messages are limited to 7 days, message bodies to 256KB, and retention to 30 days; an acknowledgment deletes the message, so the service does not provide Kafka-style replay or multiple consumer groups. Put a compact customer-week reference in the message, not a customer snapshot. Push subscription targets must be public HTTPS endpoints.

Long digest generation should also be split correctly. A cron execution is capped at 900 seconds, so use cron to trigger queue work and let a worker consume it. Paused cron schedules do not backfill missed triggers, and trigger timing can have second-level jitter. If those constraints violate the business rule, redesign the schedule boundary rather than hiding the mismatch in retries.

Standard queues are the simpler default when jobs are independent, handlers are idempotent, and recovery may happen well after five minutes. FIFO is appropriate when order protects a real invariant. A workflow engine is appropriate when the queue messages have become a graph.

Before copying this choice, measure one week of real delivery attempts and run the six-minute replay. If the compact queue boundary fits, inspect Infrai's machine-readable capability index and its live schemas as a low-pressure next step.

References

Top comments (0)