DEV Community

Kaelvyn47
Kaelvyn47

Posted on

Telemetry Retention for Scheduled Daily Report Email Retries Across Cron and Queue

Short answer: use cron as the clock for a scheduled report email, then introduce a queue only when generation or delivery may run long or must be retried; make every send idempotent, and retain compact outcome records rather than every attempt log. For a healthtech SaaS sending a weekly digest to active customers, that boundary keeps the scheduler simple while making duplicate delivery a property the backend can control.

The observability bill is mostly multiplication: customers x attempts x events per attempt x bytes per event x retention. Retry architecture changes more than delivery behavior. It changes how many events exist, which labels appear on them, and how long an operator is tempted to keep them.

Keep that multiplication visible.

What makes a scheduled email telemetry bill grow?

Start with a planning model, not a vendor price sheet. Suppose a weekly run selects 10,000 active customers. At 52 runs per year, that is 520,000 intended digests. If each attempt emits eight 1 KB events, the successful first-attempt path creates 4.16 GB of raw event payload per year before indexes, replicas, or metadata. This is an illustrative capacity model, not a measured benchmark, but the arithmetic is exact: 10,000 x 52 x 8 x 1 KB. Replace each assumption with a measured value from the application before using it for a budget.

Retries multiply the attempt term. Labels multiply the index term. A label such as customer_id can approach 10,000 values in this example; a label such as week adds another dimension; a label containing an idempotency key can approach one value per intended digest. Those fields may be useful for a targeted lookup, but promoting all of them to indexed labels is an expensive default. I would index low-cardinality state such as outcome and attempt_bucket, while keeping the customer and idempotency identifiers in the event body or in a purpose-built delivery ledger. The dominant change is to stop logging every stage at the same fidelity. Emit one compact terminal record for each intended digest, retain aggregate counters for longer analysis, and sample successful intermediate events. Keep failed and exhausted attempts at full fidelity for a shorter investigation window. I'm not sure what sampling rate fits your incident volume; the missing evidence is the number of successful attempts an engineer actually needs to reconstruct a representative run. Measure that before fixing the rate.

How should a scheduled daily report email backend split cron and queue work?

Cron should own time. A queue should own asynchronous work only when that work can outlive the scheduler's execution window or needs independent retries. For the weekly healthtech digest, cron can call a public HTTP endpoint, the endpoint can select the active-customer set and publish one job per digest, and workers can generate and send messages. The same boundary answers the daily-report query: if the complete run reliably finishes inside 900 seconds and retry requirements are modest, cron alone is simpler. If generation or a large send may exceed 900 seconds, cron should enqueue and return.

Public reachability is part of the decision. The cron target must be a public http_url, and a push subscriber must be a public HTTPS endpoint. A private-only worker ingress therefore needs a different delivery arrangement. There is also second-level trigger jitter, and pausing cron does not backfill missed triggers. These are scheduling semantics, not reasons to add more logs.

Option Strong fit Retry and retention consequence When to choose something else
Infrai cron plus standard queue A public HTTP trigger and at-least-once worker delivery through one REST API, with no SDK required The application contract can stay fixed when the provider behind the capability changes; one key and one bill also reduce integration bookkeeping. Standard-queue consumers still need idempotency. Choose an orchestrator for DAGs or joins, or another system when endpoints cannot be public.
RabbitMQ Explicit consumer acknowledgements in a broker-based design Acknowledgement state makes delivery handling visible; telemetry policy remains the application's responsibility. Avoid adding broker operations for a small job that always completes in one cron run.
Kafka Event retention, replay, and multiple consumer groups Replay changes the storage model from short-lived work items to a retained event log. A work queue is leaner when each digest should be acknowledged and deleted.
Airflow Scheduled DAGs with task dependencies Workflow state is richer than a trigger-plus-worker model and produces more states worth observing. A single fan-out with no join does not require a DAG engine.
Temporal Long-running workflow orchestration Durable workflow histories require a deliberate history and telemetry policy. A bounded email fan-out may not justify a workflow runtime.
BullMQ or Celery An application team already standardizing on one of these task queues Either keeps retry state near the worker stack; validate its delivery contract before setting retention. Do not introduce another runtime merely to replace a bounded cron call.

This comparison is about semantics, not a universal ranking. RabbitMQ, Kafka, Airflow, Temporal, BullMQ, and Celery solve adjacent but materially different problems. Don't select a retained log or workflow engine merely to obtain retries.

For this workflow, Infrai's additional practical advantage is one key and one bill across capabilities. That does not improve delivery semantics, but it reduces credential and invoice bookkeeping when a scheduler hands work to a queue, while the single REST contract keeps application code unchanged if the provider behind a capability moves.

The following call lists configured cron jobs through the verified GET /v1/cron/list route. Set INFRAI_API_ORIGIN to the service API origin and keep the key in INFRAI_API_KEY. The explicit method avoids an implicit client default, --fail-with-body surfaces a 4xx response body, and curl's retry handling covers HTTP 429 without a tight loop.

curl --request GET \
  --fail-with-body \
  --retry 4 \
  --retry-all-errors \
  --header "Authorization: Bearer ${INFRAI_API_KEY}" \
  "${INFRAI_API_ORIGIN}/v1/cron/list"
Enter fullscreen mode Exit fullscreen mode

Retry contracts need one durable identity

A standard queue provides at-least-once delivery, so duplicate delivery is expected behavior. The email send path must be idempotent. For this digest, define the business identity before publishing: a stable tuple such as customer ID plus digest period plus template revision. Persist that identity in a delivery ledger with a state transition that can distinguish reserved, sent, and retryable work. The worker checks or reserves the identity before sending; a redelivery observes the existing terminal state and does not send again.

The key point is narrow: queue acknowledgement is not proof that an email was sent exactly once. If a worker sends the email and loses execution before acknowledging the message, the queue may deliver it again. Conversely, acknowledging before the side effect risks losing the digest if the send never completes. An idempotent business operation closes that gap better than an optimistic log query.

Infrai's platform convention supports an Idempotency-Key header with a 24-hour default deduplication window, but the application's weekly digest identity should remain durable in its own ledger because business duplication can matter beyond a transport window. FIFO deduplication is only five minutes, while a standard queue remains at-least-once. This is also why I would count duplicate suppressions as a metric rather than store every suppression as a long-lived indexed event.

Keep retries bounded and observable. Record attempt count, final outcome, and a coarse error class. Handle rate limits by backing off exponentially and honoring Retry-After when it is present. Do not attach raw message bodies to retry logs: queue messages are limited to 256 KB, and copying payloads into every event increases both exposure and storage without improving the decision about the next attempt.

One terminal record is enough most days.

What should we deliberately stop keeping?

Keep the delivery ledger for the business period required by the product, but separate it from high-volume diagnostic telemetry. Queue retention can be configured only up to 30 days, and acknowledgement deletes a message, so the queue is not an audit archive. Run-history output retains only the first 4 KB. Neither should be treated as the canonical evidence of who received a digest.

For telemetry, retain aggregate counts by run and outcome, full failure details for a short diagnostic window, and a sample of successful traces. Drop successful per-stage debug events after that window. Do not index customer IDs, message IDs, or idempotency keys unless query evidence shows that the index is necessary. A narrower label set reduces cardinality; a shorter success-event window reduces stored bytes. Both reductions are intentional.

The catch is forensic depth. Aggressive sampling can hide a rare timing sequence, and deleting successful intermediate events means an old complaint may be answerable only from the terminal ledger, not from a complete execution trace. Your mileage may vary. Keep more when investigations routinely cross the chosen window; keep less when the ledger plus aggregate counters answer the operational questions.

There are hard capability boundaries too. The queue delay limit is seven days, so it should not replace the weekly cron clock. There is no native debounce or throttle, no topic-style one-to-many delivery, and no fan-out/join primitive. Stick with Airflow or Temporal when the digest is really a dependency graph with joins and recovery across stages. Stick with Kafka when replay and multiple consumer groups are requirements. For private-only endpoints, select infrastructure that can reach that network rather than exposing an endpoint solely to satisfy a scheduler.

That is the trade: fewer bytes and fewer labels lower the observability burden, but they also reduce the past you can inspect. Make the loss explicit, attach it to a retention decision, and review the decision after real incidents rather than retaining everything forever.

References

Further reading

Top comments (0)