<?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[Low Battery Tools]]></title><description><![CDATA[Low Battery Tools]]></description><link>https://lowbatterytools.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 29 Oct 2026 12:15:53 GMT</lastBuildDate><atom:link href="https://lowbatterytools.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Clocks change Oct 25 (EU) / Nov 1 (US): which of your cron jobs run twice or skip?]]></title><description><![CDATA[Disclosure: I build Cron Toolkit and Cron Heartbeat, the tools linked below. They have free tiers. Every schedule shown here was computed with the Cron…]]></description><link>https://lowbatterytools.hashnode.dev/clocks-change-oct-25-eu-nov-1-us-which-of-your-cron-jobs-run-twice-or-skip</link><guid isPermaLink="true">https://lowbatterytools.hashnode.dev/clocks-change-oct-25-eu-nov-1-us-which-of-your-cron-jobs-run-twice-or-skip</guid><category><![CDATA[cron]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Low Battery Tools]]></dc:creator><pubDate>Fri, 23 Oct 2026 18:50:32 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Disclosure:</strong> I build Cron Toolkit and Cron Heartbeat, the tools linked below. They have free tiers. Every schedule shown here was computed with the Cron Toolkit engine, and you can paste them into the free <a href="https://cron-toolkit.vendorpapers.workers.dev/cron-timezone?expr=30+2+*+*+*&amp;dialect=unix&amp;tz=Europe/Berlin">timezone page</a> to check them yourself.</p>
</blockquote>
<p>Europe falls back on <strong>Sunday 25 October 2026</strong>: at 03:00 CEST the clocks go back to 02:00 CET. The US and Canada do the same on <strong>Sunday 1 November 2026</strong>, when 02:00 local time becomes 01:00. Each of those nights has one wall-clock hour that happens twice. If your cron jobs run in a local timezone rather than UTC, that hour can make a job run twice or not at all, depending on which scheduler you use.</p>
<h2>1. A fixed-time job inside the repeated hour</h2>
<p><code>30 2 * * *</code> in Europe/Berlin:</p>
<table>
<thead>
<tr>
<th>Local run</th>
<th>UTC</th>
</tr>
</thead>
<tbody><tr>
<td>2026-10-24 02:30 (+02:00)</td>
<td>00:30Z</td>
</tr>
<tr>
<td><strong>2026-10-25 02:30 (+02:00) — runs once</strong></td>
<td>00:30Z</td>
</tr>
<tr>
<td>2026-10-26 02:30 (+01:00)</td>
<td>01:30Z</td>
</tr>
</tbody></table>
<p>The US version is <code>30 1 * * *</code> in America/New_York. It runs at 01:30 EDT on 1 November, then at 01:30 EST every day after that. It runs <strong>once</strong>, at the first 01:30.</p>
<p>That is how Vixie cron and cronie behave. <strong>Not every scheduler does this:</strong></p>
<table>
<thead>
<tr>
<th>Scheduler</th>
<th>Fixed time in repeated hour</th>
<th>Time in the spring gap</th>
</tr>
</thead>
<tbody><tr>
<td>Vixie cron / cronie</td>
<td>once</td>
<td>runs right after the gap</td>
</tr>
<tr>
<td>Kubernetes CronJob (<code>timeZone:</code>)</td>
<td><strong>can run twice</strong></td>
<td>skipped</td>
</tr>
<tr>
<td>BusyBox crond</td>
<td><strong>can run twice</strong></td>
<td>skipped</td>
</tr>
<tr>
<td>Quartz</td>
<td>once (at the <em>second</em> occurrence)</td>
<td>varies by trigger</td>
</tr>
<tr>
<td>AWS EventBridge Scheduler</td>
<td>once</td>
<td>skipped</td>
</tr>
<tr>
<td>GitHub Actions (<code>timezone:</code>, since Mar 2026)</td>
<td>once</td>
<td>runs at the gap end</td>
</tr>
</tbody></table>
<p><em>(Behaviour as documented or reported by each project. Test your own version before relying on it.)</em></p>
<p>If a job isn't idempotent, such as billing, emails or ledger exports, a scheduler that runs it twice is a real bug.</p>
<h2>2. Wildcard-hour jobs run in both passes, and that's correct</h2>
<p><code>30 * * * *</code> in Europe/Berlin on the night of 24–25 October:</p>
<pre><code class="language-plaintext">00:30 +02:00  (22:30Z)
01:30 +02:00  (23:30Z)
02:30 +02:00  (00:30Z)
02:30 +01:00  (01:30Z)   &lt;- same wall time again
03:30 +01:00  (02:30Z)
</code></pre>
<p>An hourly job should still run every real hour, so you get 25 runs that day. That's fine for hourly jobs. It confuses dashboards that key on local time, though, because two runs share the label "02:30".</p>
<h2>3. Spring forward: the job that never runs</h2>
<p>Europe springs forward on 28 March 2027, when 02:00 becomes 03:00. <code>30 2 * * *</code> Berlin:</p>
<pre><code class="language-plaintext">2027-03-27 02:30 +01:00
2027-03-28 (no 02:30 exists; skipped)
2027-03-29 02:30 +02:00
</code></pre>
<p>Our engine skips the nonexistent time. Vixie cron and GitHub Actions instead run it at 03:00. Neither approach is wrong, but you should know which one your scheduler uses.</p>
<h2>4. Midnight jobs across a 25-hour day</h2>
<p><code>0 0 * * *</code> in New York runs at 2026-11-01 00:00 −04:00, then at 2026-11-02 00:00 −05:00. The gap between those runs is 25 hours. If you check for a daily job with a "has it run in the last 24h?" alarm, that alarm will fire falsely on 2 November.</p>
<h2>How to protect yourself before Sunday</h2>
<ol>
<li><strong>Use UTC where you can.</strong> Set <code>CRON_TZ=UTC</code>, set Kubernetes <code>timeZone: Etc/UTC</code>, or leave the GitHub <code>timezone:</code> key out.</li>
<li><strong>Move local-time jobs out of 01:00–03:00.</strong> That window covers both EU and US transitions.</li>
<li><strong>Make jobs idempotent.</strong> For example, take a lock or check "already ran for date X".</li>
<li><strong>Preview the actual runs.</strong> The <a href="https://cron-toolkit.vendorpapers.workers.dev/cron-timezone?expr=30+1+*+*+*&amp;dialect=unix&amp;tz=America/New_York">cron timezone page</a> lists the next runs with their UTC offsets for unix, Quartz, AWS and GitHub syntax.</li>
<li><strong>Monitor with schedule-aware heartbeats.</strong> A monitor that knows the cron expression and timezone expects the 25-hour day, so it won't send a false alarm. <a href="https://cron-heartbeat.vendorpapers.workers.dev/docs">Cron Heartbeat</a> is mine. <a href="http://Healthchecks.io">Healthchecks.io</a> and Cronitor also do this.</li>
</ol>
<p>Have you been hit by a DST double-run? Tell me which scheduler it was in the comments. I'm collecting cases.</p>
]]></content:encoded></item><item><title><![CDATA[Your cron job didn't fail. It never ran. Detecting silent misses (including GitHub Actions' late schedules)]]></title><description><![CDATA[Most job monitoring watches for errors: non-zero exit codes, exceptions, error logs. That catches jobs that run and break. It can't catch the job that…]]></description><link>https://lowbatterytools.hashnode.dev/your-cron-job-didn-t-fail-it-never-ran-detecting-silent-misses-including-github-actions-late-schedules</link><guid isPermaLink="true">https://lowbatterytools.hashnode.dev/your-cron-job-didn-t-fail-it-never-ran-detecting-silent-misses-including-github-actions-late-schedules</guid><category><![CDATA[cron]]></category><category><![CDATA[Devops]]></category><category><![CDATA[monitoring]]></category><category><![CDATA[GitHub Actions]]></category><dc:creator><![CDATA[Low Battery Tools]]></dc:creator><pubDate>Fri, 23 Oct 2026 18:48:18 GMT</pubDate><content:encoded><![CDATA[<p>Most job monitoring watches for <strong>errors</strong>: non-zero exit codes, exceptions, error logs. That catches jobs that run and break. It can't catch the job that <strong>never starts</strong>:</p>
<ul>
<li>a deploy overwrote the crontab;</li>
<li>the container image has no cron daemon;</li>
<li>GitHub disabled the scheduled workflow after 60 days without activity on a public repo;</li>
<li>GitHub's <code>schedule</code> trigger started the run hours late, or dropped it under load (people reported 3–5 hour delays in September 2026);</li>
<li>the server was rebooting at 02:00;</li>
<li>DST: a 02:30 job doesn't exist on spring-forward day in most schedulers. On fall-back day, Kubernetes CronJob can run it <strong>twice</strong>.</li>
</ul>
<p>Nothing errors in any of these cases, so nothing alerts. You find out when someone asks where last week's report went.</p>
<h2>Invert the signal: heartbeats</h2>
<p>A heartbeat ("dead man's switch") flips the logic. The job reports success, and <strong>silence</strong> triggers the alert.</p>
<pre><code class="language-bash">0 2 * * * /usr/local/bin/backup.sh &amp;&amp; curl -fsS -m 10 --retry 3 https://monitor.example/ping/SECRET
</code></pre>
<p>The monitor knows the schedule. If no ping arrives by <em>next scheduled time + grace</em>, it alerts you, and it alerts again when the job recovers.</p>
<h3>Four details that decide whether it works</h3>
<ol>
<li><p><strong>Use the real schedule, not "every 24 h".</strong> A weekday job checked as "every 24 h" false-alarms every Monday morning. A job in <code>Europe/Berlin</code> checked in UTC is an hour off for half the year. The monitor should evaluate the same cron expression in the same timezone.</p>
</li>
<li><p><strong>Grace = the longest normal runtime + margin.</strong> For GitHub Actions, start at <strong>one hour</strong>. Late scheduled runs are normal there, and a monitor that cries wolf gets muted.</p>
</li>
<li><p><strong>Report failures explicitly</strong> so you don't wait for the deadline:</p>
<pre><code class="language-bash">job.sh &amp;&amp; curl -fsS .../ping/SECRET || curl -fsS .../ping/SECRET/fail
</code></pre>
</li>
<li><p><strong>Never let monitoring break the job.</strong> Add <code>|| true</code> around pings in scripts, and keep the ping URL secret (it's a capability).</p>
</li>
</ol>
<h2>GitHub Actions specifically</h2>
<pre><code class="language-yaml">on:
  schedule:
    - cron: "17 6 * * 1-5"         # avoid :00, the busiest minute
      timezone: "America/Chicago"   # supported since March 2026
  workflow_dispatch:
jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - run: ./make-report.sh
      - name: Heartbeat
        if: success()
        run: curl -fsS -m 10 --retry 3 "${{ secrets.HEARTBEAT_URL }}"
      - name: Report failure
        if: failure()
        run: curl -fsS -m 10 "${{ secrets.HEARTBEAT_URL }}/fail"
</code></pre>
<p>If the job must run at an exact time, add an external clock that calls <code>workflow_dispatch</code>, and make the workflow check "already done today?" first. The heartbeat then tells you if <em>both</em> paths failed.</p>
<h2>Also verify the result</h2>
<p>A backup that "succeeds" with a 40-byte file passes every exit-code check. Before pinging, check the artifact (<code>gzip -t</code>, size versus yesterday, a row count). The ping then means "a good backup exists", not just "a script ran".</p>
<h2>Tools</h2>
<ul>
<li><a href="http://Healthchecks.io"><strong>Healthchecks.io</strong></a>: open source, self-hostable, 20 free checks, email and lots of integrations. If you're unsure, start here.</li>
<li><strong>Cronitor, Dead Man's Snitch, Better Stack</strong>: hosted, polished dashboards.</li>
<li><strong>Cron Heartbeat Monitor</strong>. <em>Disclosure: I built this one.</em> It's a small API-first option that natively understands <strong>Unix, Quartz, AWS EventBridge and GitHub Actions</strong> schedules (including <code>timezone:</code>), applies a 1-hour default grace to GitHub checks, and sends webhook alerts compatible with Slack and Discord. 5 checks are free and 100 checks cost $5/month. It has no email, SMS or dashboard, and it's best-effort with no SLA. Docs: <a href="https://cron-heartbeat.vendorpapers.workers.dev/docs">https://cron-heartbeat.vendorpapers.workers.dev/docs</a></li>
</ul>
<p>If you just want to sanity-check an expression or see how a schedule behaves across this weekend's DST change (EU 25 Oct, US 1 Nov), there's a free checker with per-timezone next runs: <a href="https://cron-toolkit.vendorpapers.workers.dev/cron-timezone">https://cron-toolkit.vendorpapers.workers.dev/cron-timezone</a></p>
<hr />
<p><em>Checklist version of this post:</em> <a href="https://cron-toolkit.vendorpapers.workers.dev/monitor-cron-jobs">https://cron-toolkit.vendorpapers.workers.dev/monitor-cron-jobs</a></p>
]]></content:encoded></item><item><title><![CDATA[Converting Quartz and AWS EventBridge cron to Unix crontab (and the 4 traps that bite)]]></title><description><![CDATA[Disclosure: I maintain Cron Toolkit, the small API linked at the end. Everything in this post works without it. "Cron" means at least four different…]]></description><link>https://lowbatterytools.hashnode.dev/converting-quartz-and-aws-eventbridge-cron-to-unix-crontab-and-the-4-traps-that-bite</link><guid isPermaLink="true">https://lowbatterytools.hashnode.dev/converting-quartz-and-aws-eventbridge-cron-to-unix-crontab-and-the-4-traps-that-bite</guid><category><![CDATA[cron]]></category><category><![CDATA[Devops]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[Low Battery Tools]]></dc:creator><pubDate>Fri, 23 Oct 2026 18:22:04 GMT</pubDate><content:encoded><![CDATA[<p><em>Disclosure: I maintain Cron Toolkit, the small API linked at the end. Everything in this post works without it.</em></p>
<p>"Cron" means at least four different formats. When you move a job from Spring/Quartz or AWS EventBridge to a crontab, a Kubernetes CronJob or GitHub Actions, these are the differences that break things quietly.</p>
<h2>1. Field count: seconds and year</h2>
<table>
<thead>
<tr>
<th>Format</th>
<th>Fields</th>
</tr>
</thead>
<tbody><tr>
<td>Unix / crontab / K8s</td>
<td><code>min hour dom month dow</code></td>
</tr>
<tr>
<td>Quartz (Spring <code>@Scheduled</code>)</td>
<td><code>sec min hour dom month dow [year]</code></td>
</tr>
<tr>
<td>AWS EventBridge</td>
<td><code>cron(min hour dom month dow year)</code></td>
</tr>
</tbody></table>
<p>To convert Quartz to Unix, drop the seconds field. That only works if seconds is <code>0</code>; Unix cron can't run more often than once a minute.</p>
<h2>2. <code>?</code> is mandatory in Quartz and AWS, and invalid in Unix</h2>
<p>Quartz and AWS require <code>?</code> in either day-of-month or day-of-week. Unix uses <code>*</code>.</p>
<pre><code class="language-plaintext">Quartz: 0 0/30 8-18 ? * MON-FRI
Unix:   0,30 8-18 * * 1-5
</code></pre>
<h2>3. Day-of-week numbering is shifted</h2>
<p>Unix: <code>0</code>=Sunday (and <code>7</code> too). Quartz and AWS: <code>1</code>=Sunday, <code>7</code>=Saturday. So Quartz <code>? * 1</code> is Unix <code>* * 0</code>. Copying the number unchanged moves your job to a different day. Use <code>MON</code>-<code>SUN</code> names wherever the target format accepts them.</p>
<h2>4. Timezones</h2>
<ul>
<li>Crontab uses the server's timezone (or <code>CRON_TZ</code>, where supported).</li>
<li>GitHub Actions <code>schedule:</code> is <strong>always UTC</strong>, and it won't run jobs more often than every 5 minutes.</li>
<li>EventBridge <em>Rules</em> run in UTC. EventBridge <em>Scheduler</em> accepts a timezone.</li>
<li>K8s CronJob has <code>spec.timeZone</code> (1.27+).</li>
</ul>
<p>A <code>0 9 * * 1-5</code> meant for 9 AM Chicago fires at 3 or 4 AM Chicago on GitHub, depending on DST.</p>
<h2>Worked examples</h2>
<table>
<thead>
<tr>
<th>From</th>
<th>To</th>
</tr>
</thead>
<tbody><tr>
<td>Quartz <code>0 0 12 ? * 1</code></td>
<td>Unix <code>0 12 * * 0</code> (Sunday noon)</td>
</tr>
<tr>
<td>AWS <code>cron(0 12 ? * MON-FRI *)</code></td>
<td>Unix <code>0 12 * * 1-5</code></td>
</tr>
<tr>
<td>Unix <code>30 2 * * 0</code></td>
<td>Quartz <code>0 30 2 ? * 1</code></td>
</tr>
<tr>
<td>Unix <code>0 9 * * 1-5</code></td>
<td>AWS <code>cron(0 9 ? * 2-6 *)</code></td>
</tr>
</tbody></table>
<p>Quartz <code>L</code>, <code>W</code> and <code>#</code> (last day, nearest weekday, nth weekday) have <strong>no Unix equivalent</strong>. You need a wrapper script that checks the date, or a scheduler that supports them.</p>
<h2>Check before you deploy</h2>
<p>Always list the next few run times in the timezone the scheduler actually uses. It catches all four traps above.</p>
<p>Free browser tools (no signup): <a href="https://cron-toolkit.vendorpapers.workers.dev/quartz-to-unix">Quartz → Unix</a> · <a href="https://cron-toolkit.vendorpapers.workers.dev/aws-cron-to-unix">AWS → Unix</a> · <a href="https://cron-toolkit.vendorpapers.workers.dev/github-actions-cron">GitHub Actions checker</a> · <a href="https://cron-toolkit.vendorpapers.workers.dev/cron-to-english">cron to English</a>. If you need this in code (CI linting, admin UIs), there's an API with a free 1,000-calls/month plan: <a href="https://rapidapi.com/lowbatterytools2/api/cron-toolkit2">Cron Toolkit on RapidAPI</a>. It doesn't support L/W/# yet either.</p>
]]></content:encoded></item></channel></rss>