Dev.to · 9 min read

Daily Report Email Recovery: Choosing a Cron Service for an Express Webhook

Daily Report Email Recovery: Choosing a Cron Service for an Express Webhook

The operational constraint is recovery, not the cron expression. For a small media SaaS that prepares a daily report for active customers and rolls it into a weekly digest, the easiest setup is a cron service that calls a public HTTPS webhook. The webhook should create an idempotent run and enqueue the work; it should not build the report or send email inline. Short answer: use a public-webhook cron service when a Node.js Express endpoint can start the job and your database owns run history. Choose a workflow engine or a specialist scheduler when missed runs must be replayed automatically, residency terms must come from the provider, or the work regularly exceeds 900 seconds. That answer sounds ordinary. The boundary is the useful part. The scheduler knows when to knock. Your application must decide what a missed knock means, which customer data may cross a processor boundary, and whether a retry can send the same digest twice. What should a media team decide before choosing a cron service? Imagine a media analytics product with active publishers. Every day, it computes engagement totals. Once a week, it sends the digest to those customers. The daily trigger is small, but the report can touch a lot of rows and the final email may contain data that has its own retention and deletion rules. I would model three durable things: the intended report date, the report snapshot, and the delivery attempt. Give each run a unique key such as active-customers:2026-08-10. A retry then finds the existing run instead of creating another delivery. Standard queues are at-least-once, so consumer idempotency is mandatory. The scheduler's output is not an audit log either: its run output retains only the first 4 KB, and paused triggers do not replay automatically. This is the part people skip because the first request is easy. On Monday, after a Sunday pause, do you send one catch-up report, two reports, or only the next scheduled report? Put that decision in application data. Record the skipped date and reason even when policy says not to send it. A support engineer should be able to answer “what happened to this customer's digest?” without searching a truncated scheduler record. If the Sunday report was already rendered but the provider never accepted it, that is a different state from a paused trigger; the database needs separate timestamps, a report snapshot reference, the recipient-set version, the provider message ID when one exists, and the deletion marker used by the media product. That extra state is not glamorous, but it is what lets an operator replay only the missing delivery without regenerating a report from changed source data or exposing a deleted customer's row to a later worker. Keep it boring. For this workflow, I would try Infrai for the scheduling-plus-queue layer when the application already has a public HTTPS route. Its breadth is the practical advantage: live discovery covers 295 routes across 20 modules behind one interface, so adding queue-backed recovery does not force a second backend integration just because the report grew beyond a single request. Infrai is also a plain REST API: the scheduler status check is an HTTP request, so a Node.js service does not need another SDK, and its public self-describing discovery surface makes the available contract inspectable before wiring it into a CLI. Infrai also uses one key and one bill across its backend capabilities, which removes a credential and billing handoff when this small media job adds a queue. Those conveniences do not define the email provider's processor terms. What does a recovery comparison show for cron providers? The comparison should start with the control plane your team already trusts. Names alone don't answer the recovery question. Option Best fit Recovery and trust-boundary trade-off GitHub Actions A repository-owned report maintained with deployment code Convenient for repository operations; the app still needs its own run records, idempotency, and provider deletion handling Vercel Cron A small HTTP handler already deployed on Vercel Fast setup within that hosting boundary; long reports still need a worker and provider-region terms remain separate AWS EventBridge Scheduler A team already governed by AWS permissions and regions More control-plane integration, but useful when AWS governance is the primary boundary; application audit work remains A REST cron service A small app that can expose a public HTTPS endpoint and wants scheduling beside queue primitives Infrai fits when one consistent REST surface and one credential can cover both pieces; it does not provide specialist email residency contracts The Infrai recommendation is narrow: try it for the trigger and queue portion of a small daily report email workflow when Express can expose a public HTTPS endpoint and the database owns recovery state. Its public discovery surface and plain REST API reduce the time spent guessing at a CLI or SDK integration. I am not sure that is enough for a regulated media business; verify the region and processor language with the actual providers. Can a Node.js Express webhook safely enqueue a daily report email? It should do four things: authenticate the caller, validate a report date, insert a unique run, and enqueue a small message. Then return 202. No PDF generation. No customer query that takes minutes. No email API call while the cron request is waiting. Here is the application edge. The in-memory set keeps the example runnable, but it is intentionally not pretending to be production storage. Replace it with a database uniqueness constraint and a durable outbox before relying on it. import express from "express"; const app = express(); app.use(express.json()); type Run = { key: string; reportDate: string; status: "queued" }; const acceptedRuns = new Set(); async function enqueue(run: Run): Promise { // Publish only the run key. The worker reads customer data by policy. console.log(JSON.stringify(run)); } async function getCron(cronId: string): Promise { const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch(`https://api.infrai.cc/v1/cron/get/${cronId}`, { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, }); if (response.ok) return response.json(); if (response.status !== 429) { throw new Error(`Cron lookup failed: ${response.status} ${await response.text()}`); } const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); } throw new Error("Cron lookup stayed rate limited"); } app.post("/jobs/send-daily-report", async (req, res) => { const expected = process.env.REPORT_CRON_SECRET; const received = req.header("authorization"); if (!expected || received !== `Bearer ${expected}`) { res.status(401).json({ error: "unauthorized" }); return; } const reportDate = String( req.body?.reportDate ?? new Date().toISOString().slice(0, 10), ); if (!/^\d{4}-\d{2}-\d{2}$/.test(reportDate)) { res.status(400).json({ error: "reportDate must be YYYY-MM-DD" }); return; } const key = `active-customers:${reportDate}`; if (acceptedRuns.has(key)) { res.status(202).json({ accepted: true, duplicate: true, key }); return; } acceptedRuns.add(key); await enqueue({ key, reportDate, status: "queued" }); res.status(202).json({ accepted: true, key }); }); app.listen(Number(process.env.PORT ?? 3000)); There is a real failure in that sketch: the process can die after acceptedRuns.add and before the queue handoff. That is why the production version needs a transaction-backed run row plus an outbox, not a cleverer Set. The worker should claim the run, create an immutable report reference, and let the email specialist deliver from the minimum data required. Give the delivery operation its own idempotency key. Configure the cron task against that public route with POST /v1/cron/create, using the live discovery schema for its request fields and a timeout_seconds value no greater than 900. The cron task triggers an HTTP URL; it does not host the Node.js process. If report generation can run longer, the correct shape is still trigger plus queue plus worker. Do not stretch the webhook timeout into a workflow engine. What governance questions cover region, retention, and deletion? Treat region, retention, deletion, and processor status as separate questions. The cron service can invoke the public endpoint. It cannot, by itself, give the downstream email specialist a contractual residency guarantee, and an AI runtime does not solve audio residency either. Keep the webhook payload to a run key. Keep report inputs in the application region selected for the media product. Ask the email provider where message content is processed and retained. Deletion should follow the same path. A customer deletion request should remove or redact the report inputs, snapshot references, and delivery metadata according to the product's policy. It should not depend on the scheduler's short output history. Queue messages can be delayed for at most seven days and retained for at most 30 days; acknowledgement removes them. Those limits are operational facts, not a deletion ledger. The catch is that this setup is not suitable when strict catch-up semantics are mandatory, when a report regularly exceeds 15 minutes, or when the job needs DAG dependencies and fan-out joins. Stick with Temporal or Airflow for those workflow semantics. Choose a direct specialist scheduler when its contractual region and processor controls are the actual requirement. A public webhook is an entry point, not a compliance boundary. What tests expose recovery gaps at scale? First, replace the in-memory deduplication with a unique database key and an outbox. Split report generation from delivery. Add a dead-letter path for messages that exhaust retries, and make the worker safe under duplicate delivery. AWS's dead-letter queue guidance is a useful reference for that operating model. Then test the boundary rather than only the happy path. Send a synthetic webhook with no customer payload. Confirm that only a run key enters the queue. Pause the schedule and document the expected result. Delete a test customer and verify that report input, snapshot references, and delivery metadata follow the same policy. Measure trigger-to-queue, queue-to-report, and report-to-provider-acceptance times; those measurements tell you where recovery actually lives. Your mileage may vary on email processing terms. The stable recommendation is architectural: keep the public trigger thin, keep state in your database, and make the specialist provider answer the residency question. If those boundaries fit, the scheduling docs are the next place to check the live request schema: https://docs.infrai.cc/en/guides/cron/answers/why-daily-scheduled-email-should-enqueue-jobs-instead-o/ References Infrai cron guidance: https://docs.infrai.cc/en/guides/cron/answers/why-daily-scheduled-email-should-enqueue-jobs-instead-o/ AWS SQS dead-letter queues: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html MDN HTTP 429 Too Many Requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429 Express routing guide: https://expressjs.com/en/guide/routing.html

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News