<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[delay.run notes]]></title><description><![CDATA[delay.run notes]]></description><link>https://delayrun.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 11:39:17 GMT</lastBuildDate><atom:link href="https://delayrun.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Dead letter queues for HTTP jobs: what they are and how to use one]]></title><description><![CDATA[Retries fix most failures. An endpoint was restarting, a database was briefly locked, a third-party API hit a rate limit: try again a few minutes later and…]]></description><link>https://delayrun.hashnode.dev/dead-letter-queues-for-http-jobs-what-they-are-and-how-to-use-one</link><guid isPermaLink="true">https://delayrun.hashnode.dev/dead-letter-queues-for-http-jobs-what-they-are-and-how-to-use-one</guid><category><![CDATA[architecture]]></category><category><![CDATA[backend]]></category><category><![CDATA[Reliability]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[delay.run]]></dc:creator><pubDate>Sat, 31 Oct 2026 23:00:10 GMT</pubDate><content:encoded><![CDATA[<p>Retries fix most failures. An endpoint was restarting, a database was briefly locked, a third-party API hit a rate limit: try again a few minutes later and it works. But some jobs fail every time, no matter how often you retry. A dead letter queue (DLQ) is where those jobs go so they stop retrying, don't get lost, and can be looked at by a person.</p>
<h2>The problem a DLQ solves</h2>
<p>Without a DLQ, a job that keeps failing has two bad outcomes:</p>
<ol>
<li><strong>Retry forever.</strong> It wastes resources, may spam a struggling endpoint, and clutters logs.</li>
<li><strong>Give up silently.</strong> After N attempts it's dropped. The customer never got their invoice, and nobody knows.</li>
</ol>
<p>A DLQ is a third option: after the last retry, move the job to a separate list with its payload, the error, and every attempt. Then alert someone.</p>
<h2>What should end up there</h2>
<p>Mostly failures that won't fix themselves:</p>
<ul>
<li>A 400 or 422 because the payload is invalid (a bug in the code that created the job)</li>
<li>A 404 because the endpoint was renamed or deleted</li>
<li>A 401 or 403 because credentials expired or a secret was rotated</li>
<li>A handler that crashes on one specific record</li>
</ul>
<p>Temporary failures (502, 503, timeouts, 429) usually succeed on retry and shouldn't reach the DLQ unless the outage lasts longer than the whole retry schedule.</p>
<h2>Retry, then dead-letter</h2>
<p>A typical policy:</p>
<pre><code>attempt 1: immediately
attempt 2: +1 minute
attempt 3: +30 minutes
attempt 4: +3 hours
then: move to DLQ, alert
</code></pre>
<p>Spacing retries out (backoff) gives a struggling service time to recover. The total window, here a bit over three and a half hours, should be longer than a typical outage or deploy.</p>
<p>Some failures can skip straight to the DLQ. If a 400 means the payload is malformed, retrying it three more times won't help.</p>
<pre><code class="language-python">def should_retry(status):
    if status is None:          # timeout or connection error
        return True
    if status in (408, 425, 429) or status &gt;= 500:
        return True
    return False                # 4xx: dead-letter now
</code></pre>
<h2>What to store with each dead job</h2>
<p>Enough to understand and replay it without digging through logs:</p>
<ul>
<li>Job ID and type</li>
<li>Target URL and the full request (method, headers minus secrets, body)</li>
<li>Every attempt: time, status code, response body (truncated), latency</li>
<li>When it was created and when it was dead-lettered</li>
</ul>
<h2>Working the queue</h2>
<p>A DLQ only helps if someone looks at it. A simple routine:</p>
<ol>
<li><strong>Alert on new entries</strong>, grouped so one bad deploy doesn't send a hundred messages.</li>
<li><strong>Group by error.</strong> Fifty jobs with the same 404 have one cause.</li>
<li><strong>Fix the cause</strong>: deploy the code fix, restore the endpoint, rotate the credential.</li>
<li><strong>Replay</strong> the affected jobs. Because they were stored with their original payload, replay sends the same request again.</li>
<li><strong>Discard</strong> jobs that no longer matter, deliberately, not by ignoring them.</li>
</ol>
<h2>Replay safely</h2>
<p>Replaying sends a request that may have partly succeeded the first time, for example when the handler did its work but timed out before responding. Handlers should be idempotent, using a key that identifies the work, so a replay can't do it twice.</p>
<p>Replay in batches, too. Sending five hundred stored jobs at once to an endpoint that just recovered can knock it over again.</p>
<h2>Watch the size</h2>
<p>The number of jobs in the DLQ is a useful health signal. A steady zero is normal. A sudden jump usually means a deploy broke an endpoint. A slow, steady growth means a rare bug nobody has prioritised.</p>
<h2>Do you need to build one?</h2>
<p>If you run your own queue (SQS, RabbitMQ, a database table), you build the DLQ yourself or use the broker's built-in support. If you use a hosted scheduler for HTTP jobs, check whether it has one. <a href="https://delay.run">delay.run</a> retries jobs with backoff (by default after 1 minute, 30 minutes and 3 hours), keeps a dead letter queue with replay, and logs each attempt's request, response and status code.</p>
<h2>Related reading</h2>
<ul>
<li><a href="https://delayrun.hashnode.dev/how-to-send-a-reminder-24-hours-after-signup-without-a-queue">How to send a reminder 24 hours after signup without a queue</a></li>
<li><a href="https://delayrun.hashnode.dev/how-to-run-a-cron-job-on-the-last-day-of-the-month">How to run a cron job on the last day of the month</a></li>
<li><a href="https://dev.to/delayrun/make-scheduled-job-handlers-idempotent-safe-to-run-twice-499o">Make scheduled job handlers idempotent: safe to run twice</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to send a reminder 24 hours after signup without a queue]]></title><description><![CDATA["Send a reminder email 24 hours after someone signs up" sounds simple until you try to build it. You can't sleep for a day inside a web request, and a…]]></description><link>https://delayrun.hashnode.dev/how-to-send-a-reminder-24-hours-after-signup-without-a-queue</link><guid isPermaLink="true">https://delayrun.hashnode.dev/how-to-send-a-reminder-24-hours-after-signup-without-a-queue</guid><category><![CDATA[backend]]></category><category><![CDATA[webdev]]></category><category><![CDATA[architecture]]></category><category><![CDATA[node]]></category><dc:creator><![CDATA[delay.run]]></dc:creator><pubDate>Sat, 31 Oct 2026 22:59:20 GMT</pubDate><content:encoded><![CDATA[<p>"Send a reminder email 24 hours after someone signs up" sounds simple until you try to build it. You can't <code>sleep</code> for a day inside a web request, and a server restart forgets everything in memory. Here are the realistic options, from simplest to most robust, with their tradeoffs.</p>
<h2>Option 1: a table plus a cron sweep</h2>
<p>Store when the reminder is due, and run a job every few minutes that sends whatever is due.</p>
<pre><code class="language-sql">CREATE TABLE reminders (
  id bigserial PRIMARY KEY,
  user_id bigint NOT NULL,
  due_at timestamptz NOT NULL,
  sent_at timestamptz
);
CREATE INDEX ON reminders (due_at) WHERE sent_at IS NULL;
</code></pre>
<p>At signup:</p>
<pre><code class="language-sql">INSERT INTO reminders (user_id, due_at) VALUES ($1, now() + interval '24 hours');
</code></pre>
<p>Every five minutes:</p>
<pre><code class="language-sql">UPDATE reminders SET sent_at = now()
WHERE id IN (
  SELECT id FROM reminders
  WHERE sent_at IS NULL AND due_at &lt;= now()
  ORDER BY due_at
  LIMIT 200
  FOR UPDATE SKIP LOCKED
)
RETURNING id, user_id;
</code></pre>
<p>Send an email for each returned row.</p>
<p><strong>Pros:</strong> no new infrastructure, easy to inspect, survives restarts. <strong>Cons:</strong> reminders go out up to five minutes late, and you need somewhere to run the sweep reliably. On serverless platforms, that "somewhere" is the hard part.</p>
<h2>Option 2: a job queue with delayed jobs</h2>
<p>Libraries like BullMQ (Node), Sidekiq (Ruby) and Celery (Python) support delayed jobs:</p>
<pre><code class="language-js">await reminderQueue.add("trial-reminder", { userId }, { delay: 24 * 60 * 60 * 1000 });
</code></pre>
<p><strong>Pros:</strong> precise timing, built-in retries. <strong>Cons:</strong> you now run Redis or another broker, plus worker processes, and you have to monitor both. For one reminder email, that's a lot.</p>
<h2>Option 3: ask a scheduler to call you back</h2>
<p>The third approach moves the waiting outside your app. At signup, make one HTTP request to a scheduling service saying "call this URL in 24 hours with this payload". When the time comes, it calls your endpoint, and your handler sends the email.</p>
<pre><code class="language-js">// at signup (pseudo-code; check your scheduler's API for exact fields)
await fetch(SCHEDULER_URL, {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}`, "content-type": "application/json" },
  body: JSON.stringify({
    url: "https://app.example.com/hooks/trial-reminder",
    delay: "24h",
    body: { userId, key: `trial-reminder:${userId}` },
  }),
});
</code></pre>
<p>Your endpoint:</p>
<pre><code class="language-js">app.post("/hooks/trial-reminder", verifySignature, async (req, res) =&gt; {
  const { userId, key } = req.body;
  if (await alreadyProcessed(key)) return res.json({ skipped: true });
  const user = await db.users.find(userId);
  if (user &amp;&amp; !user.hasCompletedSetup) await sendReminder(user);
  await markProcessed(key);
  res.json({ ok: true });
});
</code></pre>
<p><strong>Pros:</strong> works on serverless platforms, no broker or workers, the scheduler handles retries. <strong>Cons:</strong> depends on an external service, and your endpoint must be reachable from the internet and verify requests.</p>
<h2>Check conditions at send time, not at schedule time</h2>
<p>Notice the <code>!user.hasCompletedSetup</code> check in the handler. Whatever option you choose, decide whether to send when the reminder fires, not when it was scheduled. In 24 hours, the user may have finished setup, deleted their account, or unsubscribed. Scheduling the check rather than the email avoids awkward messages.</p>
<h2>Handle duplicates and failures</h2>
<ul>
<li><strong>Duplicates:</strong> retries happen. Use a key like <code>trial-reminder:&lt;userId&gt;</code> and skip if already processed.</li>
<li><strong>Failures:</strong> if the email provider is down, return an error so the job is retried later. Return success only after the send succeeded.</li>
<li><strong>Cancellations:</strong> if the user deletes their account, either cancel the scheduled job or make the handler check that the user still exists (simpler, and works with every option).</li>
</ul>
<h2>Which one to choose</h2>
<table>
<thead>
<tr>
<th>Situation</th>
<th>Good fit</th>
</tr>
</thead>
<tbody><tr>
<td>You already run cron and a database</td>
<td>Table + sweep</td>
</tr>
<tr>
<td>You already run a queue with workers</td>
<td>Delayed job</td>
</tr>
<tr>
<td>Serverless or no background workers</td>
<td>Scheduler callback</td>
</tr>
</tbody></table>
<h2>A hosted option</h2>
<p><a href="https://delay.run">delay.run</a> supports exactly this pattern: one API call schedules a delayed job (up to 24 hours ahead) that calls your URL, retries with backoff if your endpoint fails, and logs each attempt. Requests are HMAC-signed, so your handler can verify them.</p>
<h2>Related reading</h2>
<ul>
<li><a href="https://delayrun.hashnode.dev/how-to-run-a-cron-job-on-the-last-day-of-the-month">How to run a cron job on the last day of the month</a></li>
<li><a href="https://dev.to/delayrun/make-scheduled-job-handlers-idempotent-safe-to-run-twice-499o">Make scheduled job handlers idempotent: safe to run twice</a></li>
<li><a href="https://dev.to/delayrun/how-to-verify-hmac-sha256-signed-webhooks-in-node-and-python-2chl">How to verify HMAC-SHA256 signed webhooks in Node and Python</a></li>
<li><a href="https://telegra.ph/Heartbeat-checks-how-to-know-when-a-scheduled-job-didnt-run-10-31">Heartbeat checks: how to know when a scheduled job didn't run</a></li>
<li><a href="https://telegra.ph/How-to-log-cron-job-output-so-you-can-debug-it-later-10-31">How to log cron job output so you can debug it later</a></li>
<li><a href="https://telegra.ph/Cron-vs-systemd-timers-which-should-run-your-scheduled-jobs-10-31">Cron vs systemd timers: which should run your scheduled jobs?</a></li>
<li><a href="https://delayrun.hashnode.dev/dead-letter-queues-for-http-jobs-what-they-are-and-how-to-use-one">Dead letter queues for HTTP jobs: what they are and how to use one</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to run a cron job on the last day of the month]]></title><description><![CDATA[Standard cron has no "last day of the month" option. The day-of-month field takes numbers from 1 to 31, and months have 28, 29, 30 or 31 days. Billing…]]></description><link>https://delayrun.hashnode.dev/how-to-run-a-cron-job-on-the-last-day-of-the-month</link><guid isPermaLink="true">https://delayrun.hashnode.dev/how-to-run-a-cron-job-on-the-last-day-of-the-month</guid><category><![CDATA[cron]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Bash]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[delay.run]]></dc:creator><pubDate>Sat, 31 Oct 2026 22:58:31 GMT</pubDate><content:encoded><![CDATA[<p>Standard cron has no "last day of the month" option. The day-of-month field takes numbers from 1 to 31, and months have 28, 29, 30 or 31 days. Billing runs, monthly reports and end-of-month cleanups all need this schedule, so here are the reliable ways to get it.</p>
<h2>Why <code>0 23 31 * *</code> doesn't work</h2>
<p>That expression runs at 23:00 on the 31st, which only exists in seven months. In February, April, June, September and November it never runs. Listing <code>28-31</code> runs it up to four times in long months. Neither is what you want.</p>
<h2>Option 1: run daily and check if tomorrow is the 1st</h2>
<p>The most portable approach. Schedule the job for days 28 to 31 and let a shell test decide:</p>
<pre><code class="language-cron">0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] &amp;&amp; /usr/local/bin/month-end.sh
</code></pre>
<p>Notes:</p>
<ul>
<li><code>%</code> has a special meaning in crontab lines (it becomes a newline), so it must be escaped as <code>\%</code>.</li>
<li><code>date -d tomorrow</code> is GNU date (Linux). On macOS or BSD, use <code>date -v+1d +%d</code>.</li>
<li>Restricting to <code>28-31</code> means the check only runs four times a month at most.</li>
</ul>
<p>If the line gets long, move the logic into the script itself:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
# month-end.sh
[ "$(date -d tomorrow +%d)" = "01" ] || exit 0
echo "Running month-end tasks for $(date +%Y-%m)"
# ... actual work
</code></pre>
<pre><code class="language-cron">0 23 28-31 * * /usr/local/bin/month-end.sh
</code></pre>
<h2>Option 2: run on the 1st and process the previous month</h2>
<p>Often the simplest answer is to change the question. Instead of running at the end of a month, run just after it:</p>
<pre><code class="language-cron">5 0 1 * * /usr/local/bin/report-previous-month.sh
</code></pre>
<p>Inside the script, calculate the previous month:</p>
<pre><code class="language-bash">PREV=$(date -d "$(date +%Y-%m-01) -1 day" +%Y-%m)
echo "Building report for $PREV"
</code></pre>
<p>This has a real advantage: the month is actually over, so late-arriving data from the final evening is included. For reports and invoices, it's usually the better design.</p>
<h2>Option 3: the <code>L</code> extension</h2>
<p>Some cron implementations (Quartz in Java, some cloud schedulers and libraries) support <code>L</code> in the day-of-month field:</p>
<pre><code class="language-plaintext">0 0 23 L * ?    # Quartz: 23:00 on the last day of every month
</code></pre>
<p>Standard Vixie cron and cronie on Linux don't support it. Check your scheduler's docs before relying on it. If your code may move between environments, options 1 and 2 are safer.</p>
<h2>Option 4: systemd timers</h2>
<p>If you use systemd timers instead of cron, <code>OnCalendar</code> supports a last-day syntax with <code>~</code>:</p>
<pre><code class="language-ini">[Timer]
OnCalendar=*-*~01 23:00:00
Persistent=true
</code></pre>
<p><code>~01</code> means "the first day counting backwards from the end", which is the last day. <code>Persistent=true</code> runs a missed job after the machine boots.</p>
<h2>Time zones matter more than usual</h2>
<p>"The last day of the month" depends on time zone. At 23:00 in UTC, it's already the 1st in parts of Asia and still the evening of the last day in the Americas. Decide which time zone defines the month for your business, and make sure the server or scheduler uses it. Many cron daemons accept <code>CRON_TZ=</code> at the top of a crontab:</p>
<pre><code class="language-cron">CRON_TZ=Europe/Berlin
0 23 28-31 * * /usr/local/bin/month-end.sh
</code></pre>
<h2>Make it safe to run twice</h2>
<p>Month-end jobs are high-stakes, and a server restart or a retried run can trigger them again. Record each run by period, e.g. <code>month-end:2026-11</code>, and skip if it's already done. That turns an accidental double run into a no-op.</p>
<h2>Know when it didn't run</h2>
<p>A monthly job that silently fails won't be noticed for another month. Log the output, and alert on failure or on a missing run. A hosted scheduler like <a href="https://delay.run">delay.run</a> logs every run with status code, response body and latency, and alerts by email, Slack or Discord when a cron fails several times in a row.</p>
<h2>Quick reference</h2>
<table>
<thead>
<tr>
<th>Approach</th>
<th>Works in standard cron</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td><code>28-31</code> + tomorrow check</td>
<td>Yes</td>
<td>Escape <code>%</code>; GNU vs BSD <code>date</code></td>
</tr>
<tr>
<td>Run on the 1st for previous month</td>
<td>Yes</td>
<td>Simplest; includes late data</td>
</tr>
<tr>
<td><code>L</code></td>
<td>No (Quartz and some services)</td>
<td>Check your scheduler</td>
</tr>
<tr>
<td>systemd <code>*-*~01</code></td>
<td>systemd only</td>
<td>Supports catch-up runs</td>
</tr>
</tbody></table>
<h2>Related reading</h2>
<ul>
<li><a href="https://dev.to/delayrun/make-scheduled-job-handlers-idempotent-safe-to-run-twice-499o">Make scheduled job handlers idempotent: safe to run twice</a></li>
<li><a href="https://dev.to/delayrun/how-to-verify-hmac-sha256-signed-webhooks-in-node-and-python-2chl">How to verify HMAC-SHA256 signed webhooks in Node and Python</a></li>
<li><a href="https://telegra.ph/Heartbeat-checks-how-to-know-when-a-scheduled-job-didnt-run-10-31">Heartbeat checks: how to know when a scheduled job didn't run</a></li>
<li><a href="https://telegra.ph/How-to-log-cron-job-output-so-you-can-debug-it-later-10-31">How to log cron job output so you can debug it later</a></li>
<li><a href="https://telegra.ph/Cron-vs-systemd-timers-which-should-run-your-scheduled-jobs-10-31">Cron vs systemd timers: which should run your scheduled jobs?</a></li>
<li><a href="https://delayrun.hashnode.dev/dead-letter-queues-for-http-jobs-what-they-are-and-how-to-use-one">Dead letter queues for HTTP jobs: what they are and how to use one</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Cron expression every 5 minutes, every 30 minutes, every hour: a cheat sheet that explains the fields]]></title><description><![CDATA[Most people search for a cron expression, copy the first answer and move on. That works until the schedule runs at the wrong time, or the same expression…]]></description><link>https://delayrun.hashnode.dev/cron-expression-every-5-minutes-every-30-minutes-every-hour-a-cheat-sheet-that-explains-the-fields</link><guid isPermaLink="true">https://delayrun.hashnode.dev/cron-expression-every-5-minutes-every-30-minutes-every-hour-a-cheat-sheet-that-explains-the-fields</guid><category><![CDATA[cron]]></category><category><![CDATA[Devops]]></category><category><![CDATA[backend]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[delay.run]]></dc:creator><pubDate>Tue, 27 Oct 2026 09:24:59 GMT</pubDate><content:encoded><![CDATA[<p>Most people search for a cron expression, copy the first answer and move on. That works until the schedule runs at the wrong time, or the same expression is rejected by AWS or Spring. This cheat sheet gives you the common expressions and, more usefully, explains why they work, so you can write the next one yourself.</p>
<h2>The five fields</h2>
<p>Standard (Unix) cron has five fields, separated by spaces:</p>
<pre><code class="language-plaintext">┌──────── minute        (0–59)
│ ┌────── hour          (0–23)
│ │ ┌──── day of month  (1–31)
│ │ │ ┌── month         (1–12)
│ │ │ │ ┌ day of week   (0–6, Sunday = 0; 7 is also Sunday on most systems)
│ │ │ │ │
* * * * *
</code></pre>
<p>Four operators cover almost everything:</p>
<ul>
<li><code>*</code> — every value</li>
<li><code>,</code> — a list: <code>1,15</code> means the 1st and the 15th</li>
<li><code>-</code> — a range: <code>9-17</code> means 9 through 17</li>
<li><code>/</code> — a step: <code>*/5</code> means every 5th value, starting from the lowest</li>
</ul>
<h2>The cheat sheet</h2>
<table>
<thead>
<tr>
<th>Schedule</th>
<th>Expression</th>
</tr>
</thead>
<tbody><tr>
<td>Every minute</td>
<td><code>* * * * *</code></td>
</tr>
<tr>
<td>Every 5 minutes</td>
<td><code>*/5 * * * *</code></td>
</tr>
<tr>
<td>Every 10 minutes</td>
<td><code>*/10 * * * *</code></td>
</tr>
<tr>
<td>Every 15 minutes</td>
<td><code>*/15 * * * *</code></td>
</tr>
<tr>
<td>Every 30 minutes</td>
<td><code>*/30 * * * *</code> (same as <code>0,30 * * * *</code>)</td>
</tr>
<tr>
<td>Every hour, on the hour</td>
<td><code>0 * * * *</code></td>
</tr>
<tr>
<td>Every 2 hours</td>
<td><code>0 */2 * * *</code></td>
</tr>
<tr>
<td>Every 4 hours</td>
<td><code>0 */4 * * *</code></td>
</tr>
<tr>
<td>Every day at 03:00</td>
<td><code>0 3 * * *</code></td>
</tr>
<tr>
<td>Weekdays at 09:00</td>
<td><code>0 9 * * 1-5</code></td>
</tr>
<tr>
<td>Every Monday at 08:30</td>
<td><code>30 8 * * 1</code></td>
</tr>
<tr>
<td>First day of every month at midnight</td>
<td><code>0 0 1 * *</code></td>
</tr>
</tbody></table>
<h2>Three mistakes that look right</h2>
<p><strong>1. Forgetting the minute in hourly schedules.</strong> <code>* */2 * * *</code> does not mean "every two hours". It means every minute of every second hour, which is 60 runs in a row. Pin the minute: <code>0 */2 * * *</code>.</p>
<p><strong>2. Steps that don't divide evenly.</strong> Steps restart at each boundary. <code>*/7</code> in the minute field gives :00, :07, :14 … :56, and then :00 again four minutes later. For an even 7-minute interval you need something other than cron. Steps that divide 60 (5, 10, 15, 20, 30) are safe for minutes; steps that divide 24 (2, 3, 4, 6, 8, 12) are safe for hours.</p>
<p><strong>3. Day-of-month OR day-of-week.</strong> When both fields are restricted, standard cron runs the job if <em>either</em> matches. <code>0 9 13 * 5</code> runs on every 13th and on every Friday, not only on Friday the 13th. If you need "Friday the 13th", run on Fridays and check the date in your script.</p>
<h2>Offsets: every 5 minutes, but not on the 0</h2>
<p>If many jobs run at <code>*/5</code>, they all start at :00, :05, :10 at the same moment. Spread them with a range and step:</p>
<pre><code class="language-plaintext">2-59/5 * * * *    # :02, :07, :12 … :57
</code></pre>
<p>The same trick works for hours: <code>15 1-23/2 * * *</code> runs at 01:15, 03:15, 05:15 and so on.</p>
<h2>Only during working hours</h2>
<p>Combine a step with an hour range and a weekday range:</p>
<pre><code class="language-plaintext">*/5 9-17 * * 1-5   # every 5 minutes, 09:00–17:55, Monday to Friday
</code></pre>
<p>Note that <code>9-17</code> includes the whole 17:00 hour. If the last run should be at 17:00, add a separate line <code>0 18 * * 1-5</code> or use <code>9-16</code> plus <code>0 17</code>.</p>
<h2>“Cron every 5 minutes” with 6 fields</h2>
<p>Different tools extend cron with extra fields, and copying between them is where most errors come from.</p>
<p><strong>Spring (</strong><code>@Scheduled(cron = ...)</code><strong>)</strong> adds a <strong>seconds</strong> field at the front:</p>
<pre><code class="language-plaintext">0 */5 * * * *       # second 0 of every 5th minute
</code></pre>
<p><strong>Quartz</strong> also starts with seconds, and requires <code>?</code> in either day-of-month or day-of-week:</p>
<pre><code class="language-plaintext">0 0/5 * * * ?
</code></pre>
<p><strong>AWS EventBridge / CloudWatch</strong> uses six fields with a <strong>year</strong> at the end, no seconds, and also needs <code>?</code> in one of the day fields:</p>
<pre><code class="language-plaintext">cron(0/5 * * * ? *)
</code></pre>
<p>So the same schedule has three different spellings. Before pasting an expression, check how many fields the tool expects and whether the first field is seconds or minutes.</p>
<h2>What cron cannot do</h2>
<ul>
<li><strong>Sub-minute schedules.</strong> Standard cron's smallest unit is one minute. "Every 10 seconds" needs a loop, a systemd timer or a 6-field scheduler.</li>
<li><strong>"Every N days" across months.</strong> <code>0 0 */3 * *</code> restarts on the 1st of each month, so the gap between the 31st (or 28th) and the 1st is shorter.</li>
<li><strong>Time zones per job</strong> in classic cron. The whole crontab uses the server's time zone unless your implementation supports <code>CRON_TZ</code>.</li>
</ul>
<h2>Testing an expression</h2>
<p>Before you trust a schedule, list the next few run times. Many online cron explainers do this, and in Python you can use the <code>croniter</code> package:</p>
<pre><code class="language-python">from croniter import croniter
from datetime import datetime

it = croniter("2-59/5 9-17 * * 1-5", datetime.now())
for _ in range(5):
    print(it.get_next(datetime))
</code></pre>
<p>Seeing the next five timestamps catches most of the mistakes above in seconds.</p>
<h2>Where the expression runs matters too</h2>
<p>A correct expression is only half the job; something still has to run it reliably. On a single server that is the cron daemon, with all the quirks of its environment. If your task is an HTTP endpoint, a hosted scheduler such as <a href="https://delay.run">delay.run</a> takes the same standard five-field syntax, down to once a minute, and records the response of each run.</p>
<p>Keep this page handy, and when a schedule looks odd, read it field by field from left to right. The mistake is almost always in one of the three traps above.</p>
<h2>Related reading</h2>
<ul>
<li><a href="https://telegra.ph/How-to-log-cron-job-output-so-you-can-debug-it-later-10-31">How to log cron job output so you can debug it later</a></li>
<li><a href="https://dev.to/delayrun/how-to-verify-hmac-sha256-signed-webhooks-in-node-and-python-2chl">How to verify HMAC-SHA256 signed webhooks in Node and Python</a></li>
<li><a href="https://delayrun.hashnode.dev/how-to-send-a-reminder-24-hours-after-signup-without-a-queue">How to send a reminder 24 hours after signup without a queue</a></li>
<li><a href="https://telegra.ph/Cron-vs-systemd-timers-which-should-run-your-scheduled-jobs-10-31">Cron vs systemd timers: which should run your scheduled jobs?</a></li>
</ul>
]]></content:encoded></item></channel></rss>