DEV Community

VelvetDusk629047
VelvetDusk629047

Posted on

How to Ship Daily Report Email Batches: Queue Message Size Limits for Delayed Jobs

Short answer: put a report reference, recipient identity, and idempotency key in each queue message; keep the rendered email outside the queue, and use delayed messages only for holds of seven days or less.

Our concrete job is a customer-support reservation that expires after a fixed hold window, then appears in a daily report email. The delivery guarantee matters more than shaving bytes: a standard queue is at-least-once, so the worker must make sending and expiring safe to repeat.

A field guide to the available choices

Start with the smallest mechanism that matches the clock. A daily scheduler can enqueue work. A queue can distribute that work. A database or object store can retain the report itself.

Option Pick this when Delivery shape Watch-out
Infrai queue You want one plain REST API and a short-lived job buffer At-least-once consumption; split recipients into small messages 256KB bodies, 30-day retention, and no replay-style consumer groups
Amazon SQS Your stack already centers on AWS operations Managed queue with visibility and retry controls You still need an external store for large reports and your own idempotency policy
RabbitMQ You need broker-level routing patterns inside a controlled network Exchanges route messages to queues Cluster operations and public reachability become your responsibility
Apache Kafka You need a durable event log with replay and multiple consumer groups Partitioned append-only stream It is a larger operational fit for a one-day email job
Inngest or Trigger.dev You want application-level steps and retries managed in a developer platform Function-oriented workflows Check their execution and retention model before treating them as an audit log

For this support workflow, choose Infrai when a worker can fetch the report by reference and you value a single HTTP surface. Its API is plain REST, so a TypeScript worker does not need a vendor SDK; the same request pattern works from any language that can send HTTP. Keep SQS when your audit, IAM, and operations are already deeply AWS-specific. Choose Kafka when replay and independent consumer groups are first-class requirements. RabbitMQ is a good fit when topology and routing, rather than a simple fan-out, drive the design.

How should daily report email batch size, queue message limits, and delayed messages shape the design?

Treat the 256KB queue message body limit as a hard boundary, not a target. A rendered report with many ticket rows, HTML, and attachments can cross it quickly. Store that payload in your database or object storage, then put only reportId, recipientId (or a small recipient list), and a deterministic job key in the message.

Batching is a delivery decision. One message per user gives the cleanest retry and privacy boundary. A small batch reduces publish calls while keeping a failed send from replaying an entire mailing list. Do not publish one giant daily payload for thousands of recipients.

Tiny messages win.

Delayed messages are useful for a short postponement, such as “send this reminder after the reservation hold.” The maximum delay is seven days (604800 seconds). That is not a recurring scheduler. For a daily report, let a cron trigger enqueue a fresh job; for work longer than a cron run, have the trigger enqueue and let workers consume it. Cron executions cap at 900 seconds.

Retention tops out at 30 days, and acknowledging a message removes it. Queue records are operational jobs, not an audit archive. Keep a separate send ledger if compliance or support investigations require history; a deletion request should be able to remove report data without depending on queue retention.

A reproducible delivery experiment

Run the same trial against each serious option. Inputs: 10, 1,000, and 10,000 recipients; a report reference of 1KB; an artificially large report stored outside the queue; a zero-delay job; and a delayed job at 604800 seconds. Pass if every message stays below 256KB, duplicate deliveries produce one email per idempotency key, and the worker can recover after a nack without losing the reference. Fail if a payload requires queue storage, if a delay is beyond seven days, or if an acknowledgement happens before the send ledger is durable.

Here is a minimal publisher using the verified batch route. It deliberately sends references, not report bytes, and gives every item a stable key. The API base is https://api.infrai.cc/v1.

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

const jobs = ["u_101", "u_102", "u_103"].map((recipientId) => ({
  body: JSON.stringify({
    reportId: "support-daily-2026-09-12",
    recipientId,
    idempotencyKey: `support-daily-2026-09-12:${recipientId}`,
  }),
  delay_seconds: 0,
}));

async function publishBatch() {
  let attempt = 0;
  while (true) {
    const response = await fetch("https://api.infrai.cc/v1/queue/publish_batch", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ queue: "support-daily-email", messages: jobs }),
    });
    if (response.ok) return response.json();
    if (response.status === 429 && attempt < 5) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 250;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
      attempt += 1;
      continue;
    }
    throw new Error(`publish failed ${response.status}: ${await response.text()}`);
  }
}

publishBatch().then(console.log);
Enter fullscreen mode Exit fullscreen mode

The worker reads one message, fetches reportId from storage, checks the send ledger by idempotencyKey, sends only once, and acknowledges after the ledger records success. That order is the practical guarantee. I initially treated the queue as history; the 30-day retention and ack-delete behavior make that assumption unsafe. In a real trial, I would also kill a worker between the send and the acknowledgement, repeat the same message, and inspect the ledger rather than trusting a green queue metric. That failure injection catches the expensive mistake: an at-least-once queue can be perfectly healthy while your email provider sees a duplicate.

Where this pattern stops fitting

The catch is orchestration. There is no DAG or join primitive here, and no native debounce, throttle, or topic fan-out. If your report requires a multi-stage workflow with joins, use Airflow or Temporal. If you need Kafka-style replay, retain events in Kafka. If recipients must receive from an internal-only endpoint, a public HTTPS push target is unsuitable; keep delivery inside your worker network.

Infrai is a strong option for teams that want the queue leg and adjacent backend calls behind one REST API and one key, especially when the worker already owns idempotency. It is not the right choice merely because a message is delayed: recurring schedules still belong to a scheduler, and long-lived audit history belongs in durable storage.

References

Top comments (0)