Back to blog

The Python Cron Parser Behind Airflow Is Not the Cron You Know

croniter parses the cron expressions inside Airflow and many Python schedulers. It almost got unpublished, and its 2026 bugfix releases quietly shift when your jobs fire.

CronGuard TeamCron Job Monitoring Experts
6 min read
Colorful syntax-highlighted source code on a black screen viewed at a slight angle, with a database query and functions like parseFloat and forEach visible in orange, blue and pink

The cron in your Python scheduler is a library, not crond

If your scheduling lives in Airflow, a Django management command, or a homegrown loop that wakes up and asks "should this run now?", the thing answering is almost never the cron daemon. It is croniter, a small Python library that turns a cron string into the next datetime it should fire. You wrote 0 2 * * 0 expecting what crond would do, and you get croniter's reading of it instead. The two do not always agree.

That gap matters because croniter had an eventful year. It nearly got pulled off PyPI, changed hands, and shipped bugfix releases that moved when some expressions fire. Nothing shouts when a scheduler fires an hour off or skips a day, so you may never notice.

Where croniter actually runs

Apache Airflow uses croniter to evaluate cron-based schedules, and plenty of internal schedulers and Django jobs import it to answer "when is the next run?". At roughly 46 million downloads a month on PyPI, that is a lot of schedules resting on one parser, none of them running your system crond. They run croniter's reading of the string, in whatever timezone the Python process happens to sit in.

It parses a different cron than crond does

Vixie cron, the crond on most Linux boxes, accepts five fields and a fixed set of extensions. croniter accepts more, and that is the first place expectations break.

Six fields, sometimes seven

croniter reads an optional sixth field for seconds and an optional seventh for years. */30 * * * * * is a valid every-30-seconds schedule to croniter and a syntax error to crond.

L, W, and the Jenkins-style hash

croniter supports L for the last day of the month, W for the nearest weekday to a date, and sat#1 for the first Saturday. It also takes H and R for hashed and random values, the Jenkins trick for spreading load off round numbers. None of these mean anything to Vixie cron, so an Airflow expression will not copy into a crontab unchanged.

Day-of-month and day-of-week default to OR

When both the day-of-month and day-of-week fields are restricted, POSIX cron runs the job when either one matches. croniter follows that OR behaviour by default through its day_or option. So 0 0 13 * 5 does not mean "Friday the 13th". It means the 13th, and also every Friday. Flip day_or and you get AND semantics.

A patch release can move when a job fires

Fix a bug in how croniter maps a string to datetimes and the datetimes change, which makes its patch notes something you have to read.

The leap-year skip

Version 6.2.1, on 15 March 2026, fixed get_prev skipping 29 February on leap years for day-of-month expressions. Pinned below that, a backfill walking backwards through time quietly stepped over a real day.

The Sunday-wrap fix

Version 6.2.4, on 10 July 2026, corrected the day-of-week low bound for stepped ranges so Sunday wraps properly. Cron numbers Sunday as both 0 and 7, and stepped ranges over that field are where off-by-one bugs live. A job firing on the wrong days before the fix fires on a different set after.

Quadratic expansion and stricter validation

Version 6.2.3, on 2 July 2026, fixed quadratic expansion of long comma-separated range lists and started rejecting zero steps. Version 6.2.0 added a strict flag for cross-field validation that can reject on deploy a string your scheduler accepted for years, which you want to hit in CI rather than at 2 a.m.

The dependency almost disappeared

In December 2024 the maintainer opened an issue titled "Hopefully not the end", planning to stop development and possibly unpublish the package in 2025, citing the compliance burden of the EU Cyber Resilience Act. It was rescued rather than removed: croniter now lives under pallets-eco, the community organisation behind Flask, and keeps shipping releases. Forget the specific library for a moment. The parser that decides when your jobs run is a dependency with its own release schedule, and a change in it is a change to your schedule.

Pin it, test it, and watch the actual run

Pin croniter to an exact version so an unattended upgrade cannot shift your fire times without a code review. When you upgrade, assert the fire times you expect instead of trusting the diff:

from datetime import datetime
from croniter import croniter

schedule = croniter("0 0 13 * 5", datetime(2026, 1, 1))
# The 13th AND every Friday - OR semantics, not "Friday the 13th"
print(schedule.get_next(datetime))

No local test proves the job actually ran in production, so watch the run itself. A dead man's switch that expects a check-in on the schedule you think you configured fires the moment croniter and your intent disagree, whether the cause is a dialect quirk, an upgrade, or a wrong timezone.

#!/bin/bash
set -euo pipefail

python -m myapp.nightly_rollup
curl -fsS --retry 3 https://cronguard.app/api/ping/your-monitor-id

The parser can be wrong about when. The check-in tells you whether.

Frequently asked questions about croniter and Python cron parsing

Is croniter the same as the cron daemon on my server? No. croniter is a Python library that maps a cron string to datetimes inside your process, while crond is a separate system service. They share the five-field syntax but diverge on extensions and edge cases, so the same expression can resolve differently in each.

Why would upgrading croniter change when my jobs run? Fixing a bug in how an expression maps to run times changes the set of times it produces. The 6.2.1 leap-year fix and the 6.2.4 Sunday-wrap fix both altered which dates matching expressions resolve to, so a patch upgrade can move real fire times.

Does croniter treat day-of-month and day-of-week as AND or OR? By default it uses OR, matching POSIX cron: when both fields are restricted the job runs when either one matches. So an expression meant as "Friday the 13th" runs on the 13th and on every Friday. Switch to AND with the day_or option.

Can I copy a croniter expression straight into my crontab? Not always. croniter accepts a seconds field, a year field, and extensions like L, W, the nth-weekday hash, and Jenkins-style H and R that Vixie cron does not understand. A plain five-field expression ports fine; anything using those extras will not.

Was croniter really going to be removed from PyPI? The maintainer opened an issue in December 2024 planning to end development and possibly unpublish it, citing the EU Cyber Resilience Act. pallets-eco adopted it instead and keeps shipping releases, but the episode shows the parser your schedule depends on has its own maintenance lifecycle.

Further reading


Conclusion: The cron string in your Python scheduler is interpreted by croniter, not by the daemon you tested against, and that parser has a dialect and a bug history of its own. Pin it to an exact version. Assert the fire times you expect when you upgrade. And monitor the actual run, so a silent disagreement between what you wrote and what croniter does becomes an alert instead of a missing job.

Sources: croniter on PyPI, croniter changelog, and the "Hopefully not the end" issue.

Share

Related posts

A fiber-optic patch panel in a rack, green connectors and yellow cables looping across rows of numbered ports
ReliabilityWhen a Scheduled Job Runs Long: Overlap, Skip, or Replace
A person at a desk in a dark room, lit by two monitors filled with lines of code, looking toward the camera, in black and white
ReliabilityThe Kubernetes CronJob Deadlock That Stops Your Schedule For Good
A close-up of a computer monitor showing colorful Python source code with line numbers on a dark background
Monitoringpg_cron in Production: Failure Modes Host Cron Never Had

Set up your first monitor.
It'll take 30 seconds.