◉CLAUDE LABJP
●2.1.300 — Adds virtualText for Mod prompt decorations; background commands still running 1.5 seconds after exit are now force-killed●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 46 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●CACHE — Prompt cache reads on Sonnet 5.5 dropped from $0.20 to $0.10 per million tokens (Oct 7)●NEW — I Put a Ten-Line Promise File at the To…●2.1.300 — Adds virtualText for Mod prompt decorations; background commands still running 1.5 seconds after exit are now force-killed●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 46 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●CACHE — Prompt cache reads on Sonnet 5.5 dropped from $0.20 to $0.10 per million tokens (Oct 7)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-08-31Intermediate

Half of My Scheduled Runs Vanished Without a Single Error

A batch job set to run twice a day was only firing once. No errors, no failure alerts. Here is how to expand your own schedule, count expected runs, and reconcile them against execution records to catch silent misses.

Claude Code264scheduled tasks12cron4operations30automation109

✦ Premium Article

An asset classification batch was set to run twice a day, morning and late afternoon, a few hundred images per pass.

What tipped me off was the backlog, not the logs. Throughput was running at roughly half of what the schedule implied. The error log was empty. No failure notification had arrived. Every run that did leave a record had exited cleanly, with nothing unusual in its output.

The afternoon run had never started at all.

A run that never starts never fails. Because it never fails, nothing that watches for failure will ever mention it. This piece is about making that blank space visible.

The Crontab Syntax Was Fine

The schedule was written like this:

30 4,16 * * *

As crontab syntax, that is unambiguously twice a day. Still, I wanted to count it myself rather than trust my reading, so I wrote a small expander that takes a cron expression and lists every moment a firing is expected inside a window.

#!/usr/bin/env python3
"""Expand a cron expression into the moments a firing is expected."""
from datetime import datetime, timedelta
 
 
def _field(spec: str, lo: int, hi: int) -> set:
    out = set()
    for part in spec.split(","):
        step = 1
        if "/" in part:
            part, s = part.split("/", 1)
            step = int(s)
        if part in ("*", ""):
            start, end = lo, hi
        elif "-" in part:
            a, b = part.split("-", 1)
            start, end = int(a), int(b)
        else:
            start = end = int(part)
            if step != 1:
                end = hi
        out |= set(range(start, end + 1, step))
    return {v for v in out if lo <= v <= hi}
 
 
def expected(cron: str, start: datetime, end: datetime):
    mi, ho, dom, mon, dow = cron.split()
    M, H = _field(mi, 0, 59), _field(ho, 0, 23)
    DOM, MON = _field(dom, 1, 31), _field(mon, 1, 12)
    DOW = {d % 7 for d in _field(dow, 0, 7)}      # Sunday is both 0 and 7
    dom_restricted = dom.strip() != "*"
    dow_restricted = dow.strip() != "*"
 
    t = start.replace(second=0, microsecond=0)
    while t <= end:
        if t.minute in M and t.hour in H and t.month in MON:
            d_ok = t.day in DOM
            w_ok = ((t.weekday() + 1) % 7) in DOW
            # Both day-of-month and day-of-week restricted means OR, not AND
            ok = (d_ok or w_ok) if (dom_restricted and dow_restricted) else (d_ok and w_ok)
            if ok:
                yield t
        t += timedelta(minutes=1)

Over seven days it expands to 14 firings. My reading of the syntax was correct, which meant the loss was happening somewhere below the syntax, in whatever actually launches the job.

That was the fork in the road. Rereading the config file would never have gotten me there. Once you have confirmed the configuration says what you think it says, the next thing to look at is not the configuration but the record of what happened.

One detail I would not have trusted without expanding it: when both day-of-month and day-of-week are restricted, cron treats them as OR. So 0 9 1 * 1 fires on the first of the month and on every Monday. Expanding September gives 09-01 (Tue) plus each Monday, five firings, which matched what I expected only after I saw it printed.

A Failure Class That Never Reaches Your Failure Log

Most monitoring around automation is shaped to catch failures. Check the exit code, forward the exception, inspect the output. All three require a process to have existed.

When the launch itself is what goes missing, none of that shape applies.

FailureShows in failure log?How you find out
Exception during processingYesAlert fires immediately
Crash or timeoutYes, via exit codeAlert fires
Preconditions unmet, did nothingNoIntended behaviour, no problem
Never launchedNoOnly by counting output volume

As an indie developer running long unattended jobs, I had built my monitoring entirely around the first two rows. There was no place in the design for a question about what did not happen.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You will be able to prove, from records rather than from the absence of errors, that an automated job actually ran
✦You will be able to expand your own schedule, count expected firings, and detect the gap against real execution records with a single morning command
✦You will be able to catch failures where the launch itself disappears, before the missing work quietly piles up
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Claude Code2026-08-25
One Space in a Folder Name Turned 80 Checks Into Zero
An inspection loop reported 80 files checked and 0 readable. The files were fine. Here is how word splitting turns path fragments into real directories, measured side by side, plus the count assertion I now put in front of every delete-heavy batch.
⟐ Claude Code2026-08-04
Tightening Filesystem Isolation Separately from the Network — Collect the Paths, Then Squeeze the Write Surface
Claude Code v2.1.216 lets you control filesystem isolation independently from network isolation. Before tightening anything, I traced what a real job actually touches, split reads from writes, and measured how stable the path set is across repeated runs. The numbers changed how I wrote the allowlist.
⟐ Claude Code2026-09-22
Taking Inventory of Worktrees That Were Supposed to Be Gone
Isolated worktrees my subagents created were never folded away. Here is what I measured in git 2.34.1 about when remove succeeds, the branches that outlive the trees, the 12 MB per-tree difference, and how I decide who owns cleanup.