<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Infrai</title>
    <description>The latest articles on DEV Community by Infrai (@huachen_infrai).</description>
    <link>https://dev.to/huachen_infrai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038988%2F286d098f-c24e-47ac-9658-9041bdec0c60.png</url>
      <title>DEV Community: Infrai</title>
      <link>https://dev.to/huachen_infrai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/huachen_infrai"/>
    <language>en</language>
    <item>
      <title>Infrai Cron: The Easiest Daily Report Email Setup for Node.js</title>
      <dc:creator>Infrai</dc:creator>
      <pubDate>Sat, 25 Jul 2026 13:57:15 +0000</pubDate>
      <link>https://dev.to/huachen_infrai/infrai-cron-the-easiest-daily-report-email-setup-for-nodejs-28lm</link>
      <guid>https://dev.to/huachen_infrai/infrai-cron-the-easiest-daily-report-email-setup-for-nodejs-28lm</guid>
      <description>&lt;p&gt;Use Infrai's cron service for a daily report email when your app can already expose a public webhook that kicks off the report. That's the whole trick: the cron job doesn't run your code, it just calls a public HTTPS URL on a schedule. If you've got an Express route like &lt;code&gt;/jobs/send-daily-report&lt;/code&gt;, you're most of the way there. One key, one bill, and no worker box to babysit.&lt;/p&gt;

&lt;p&gt;I reached for this after wiring the same thing on a self-managed scheduler and losing an afternoon to a container that silently stopped firing. A cron service you don't host can't drift like that. The tradeoff — and I'll be upfront — is that Infrai's cron only pokes a public endpoint; it won't execute a function for you and it won't run longer than 900 seconds per trigger. For a single daily report batch, that's plenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I run a daily report email cron in Node.js and Express?
&lt;/h2&gt;

&lt;p&gt;The mental model is two halves. Half one is your Express endpoint that generates the report and sends the email. Half two is the cron job that hits it every morning. Infrai owns only the second half.&lt;/p&gt;

&lt;p&gt;Create the schedule once. The task must be a public &lt;code&gt;http_url&lt;/code&gt; — an internal &lt;code&gt;localhost&lt;/code&gt; route or a VPC-only endpoint won't receive the call, so this is genuinely a "your app has a public API" pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scheduleDailyReport&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.infrai.cc/v1/cron/create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// ifr_..., from the env&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cron-daily-report-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// retry-safe: never double-creates&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// On HTTP 429, back off and retry (honour Retry-After) rather than hammering.&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.yourapp.com/jobs/send-daily-report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cron_expr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 7 * * *&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// 07:00 every day&lt;/span&gt;
      &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UTC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timeout_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// hard ceiling is 900&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`cron/create failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receiving side is ordinary Express. Keep it fast — you're on a 120-second budget here, and a hard 900-second ceiling overall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/jobs/send-daily-report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;               &lt;span class="c1"&gt;// ack fast, then do the work&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildReport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markReportSent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  &lt;span class="c1"&gt;// your own audit trail, see below&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the easy path end to end. Grab a key, point the cron at your URL, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the cron service will not do for you
&lt;/h2&gt;

&lt;p&gt;Honesty is what makes this worth reading, so here are the edges I'd want a teammate to know before shipping.&lt;/p&gt;

&lt;p&gt;Paused jobs don't catch up. If you pause a schedule and resume it three days later, those three missed 7am triggers are gone — Infrai won't replay them. If your reporting has strict catch-up semantics ("every day must produce exactly one report, even after an outage"), you need to reconcile that yourself against a table of which dates you've already sent.&lt;/p&gt;

&lt;p&gt;Run history is thin. The platform keeps run records, but the stored output is truncated to the first 4KB, and trigger timing has second-level jitter. So don't treat the cron run log as your source of truth. Write the email and report status into your own database — that &lt;code&gt;db.markReportSent&lt;/code&gt; line above isn't decoration, it's the audit trail you'll actually query when a customer asks where their Tuesday report went.&lt;/p&gt;

&lt;p&gt;And there's no workflow engine here. No DAGs, no fan-out/join, no Airflow-style orchestration. The moment one daily trigger needs to explode into thousands of per-customer sends, you've outgrown a bare cron and should put a queue worker between the trigger and the sends. Infrai has that queue too — &lt;code&gt;/v1/queue/publish&lt;/code&gt; and &lt;code&gt;/v1/queue/consume&lt;/code&gt; — but wiring it is a separate article.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Infrai cron compare to the alternatives?
&lt;/h2&gt;

&lt;p&gt;Plenty of good schedulers exist, and I won't pretend Infrai is the only sane pick. Inngest and Trigger.dev are excellent if you want durable, code-first workflows with steps and retries baked in. Upstash QStash is the closest analog — a hosted HTTP scheduler that also just calls your URL. AWS EventBridge fits if you're already deep in that ecosystem. Where Infrai wins is consolidation: the same key that schedules this report also sends the email, stores files, and runs your AI calls, all on one invoice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai cron&lt;/td&gt;
&lt;td&gt;Calls a public HTTPS URL on a schedule&lt;/td&gt;
&lt;td&gt;You want scheduling plus email, storage, AI on one key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstash QStash&lt;/td&gt;
&lt;td&gt;Hosted HTTP scheduler&lt;/td&gt;
&lt;td&gt;You only need to hit a URL and nothing else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inngest / Trigger.dev&lt;/td&gt;
&lt;td&gt;Durable code-first workflows&lt;/td&gt;
&lt;td&gt;You need multi-step orchestration and replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS EventBridge&lt;/td&gt;
&lt;td&gt;Native AWS event bus&lt;/td&gt;
&lt;td&gt;Your stack already lives in AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There's a reason I lean on the consolidation angle rather than raw feature count. Every backend you bolt on is another SDK to upgrade, another key to rotate, another dashboard to check when something breaks at month-end. A daily report job is small, but it rarely stays alone — soon it wants to store a PDF, then send an SMS on failure, then log a metric. When those live behind the same key, the second and third features are a body change, not a new vendor contract. That's the quiet win that doesn't show up in a feature table.&lt;/p&gt;

&lt;p&gt;For a small SaaS sending one report batch a day, the single-key story is the one I'd choose — fewer moving parts, and the discovery API is public so you can confirm every limit I listed here before you sign up. Start at &lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;docs.infrai.cc&lt;/a&gt; and the Cron Jobs reference, create your first schedule, and let your 7am self sleep in.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Infrai llms.txt machine index — &lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;https://docs.infrai.cc/llms.txt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Infrai discovery: queue.dlq.redrive schema — &lt;a href="https://api.infrai.cc/v1/discovery/queue.dlq.redrive" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/queue.dlq.redrive&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS SQS dead-letter queues — &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN: HTTP 429 Too Many Requests — &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cron</category>
      <category>node</category>
      <category>email</category>
      <category>webhook</category>
    </item>
  </channel>
</rss>
