# Cronos: a .NET cron parser that refuses to guess about daylight saving time

> Cronos is the Hangfire project's .NET library for parsing cron expressions and calculating the next and previous occurrence, with time zones and daylight saving transitions designed in rather than bolted on. It is a parser and a calculator, not a scheduler, and its API deliberately rejects local DateTime values because they can be ambiguous.

**HangfireIO/Cronos** — A fully-featured .NET library for working with Cron expressions. Built with time zones in mind and intuitively handles daylight saving time transitions

- Repository: https://github.com/HangfireIO/Cronos
- Stars: 1,191 · Forks: 136
- Language: C#
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/hangfireio-cronos

## A parser and a calculator, with no scheduler attached

Cronos is described as a fully-featured.NET library for working with Cron expressions, built with time zones in mind and intuitively handling daylight saving time transitions as in nix Cron. The README is careful about its scope, and the sentence is in italics for a reason: this library doesn't include any task or job scheduler, it only works with Cron expressions.

That boundary is what makes the library useful rather than redundant. Cronos is for the cases where the schedule is not the whole problem: a subscription that renews on a policy expression, a report that runs at a business defined time, a migration that has to run at three in the morning on the last day of the month, and where the question is only what the next occurrence is.

The features list gives a good sense of the input surface. There is support for the standard Cron format with optional seconds, and for the non-standard characters L, W and # and their combinations. There is schedule jitter through the H character, inspired by Jenkins, which spreads a job across a window instead of pinning it to an exact minute so that many instances do not all fire at the same instant. There are reversed ranges, where 23-01 is equivalent to 23,00,01 and DEC-FEB to DEC,JAN,FEB.

The project also states that it contains 1000+ unit tests to ensure everything is working correctly, and the repository carries a benchmarks directory, which is the right thing to have next to a library whose whole job is arithmetic on dates.

## The API throws if you hand it an ambiguous local time

The usage section makes an unusual design decision explicit, and it is the one thing to read before writing any code against this library.

You can only use DateTime with DateTimeKind.Utc specified, for example DateTime.UtcNow, or DateTimeOffset classes to calculate next occurrences. You cannot use local DateTime objects such as DateTime.Now, because this may lead to ambiguity during DST transitions, and an exception will be thrown if you attempt to use them.

The reasoning is sound. A local DateTime on the far side of a daylight saving transition does not name a single instant, it names a wall clock reading that occurred twice, and a scheduler that silently picks one of them will run a job an hour early or an hour late exactly twice a year. Refusing the input moves the bug to compile-time-shaped errors instead of a production incident.

The API itself is small. You create a CronExpression and call GetNextOccurrence to go forwards or GetPreviousOccurrence to search in reverse:

```csharp
using Cronos;

CronExpression expression = CronExpression.Parse("* * * * *");

DateTime? nextUtc = expression.GetNextOccurrence(DateTime.UtcNow);
DateTime? previousUtc = expression.GetPreviousOccurrence(DateTime.UtcNow);
```

The semantics are worth reading twice. The nextUtc value contains the next occurrence in UTC after the given time, or a null value when it is unreachable, for example Feb 30. The previousUtc contains the most recent occurrence in UTC before the given time, or null when nothing exists. And an invalid cron expression throws CronFormatException.

The null returns are a design choice worth noticing. A schedule that can never fire returns null instead of looping forever or throwing, which lets a caller decide whether that is a configuration error or an acceptable state.

## Three separate guarantees about daylight saving transitions

The DST behaviour is not one rule but three, and the README lists them separately because each covers a different failure mode.

Cronos does not skip occurrences when the clock jumps forward to daylight saving time. A schedule that should run at 02:30 on the spring-forward day still runs, even though 02:30 does not exist on that clock. A naive implementation drops that run, and a daily job silently skips a day every year.

It also does not skip interval-based occurrences when the clock jumps backward. If the schedule is every fifteen minutes, the runs in the repeated hour still happen.

And it does not retry non-interval based occurrences when the clock jumps backward. A daily-at-02:30 job does not run twice on the fall-back day.

There is a second, smaller piece of DST design in the time zone overloads. You can pass a time zone directly, and then either pass a DateTime with the DateTimeKind.Utc flag or use the DateTimeOffset class, since that is smart enough to always point to an exact, non-ambiguous instant:

```csharp
CronExpression expression = CronExpression.Parse("* * * * *");
TimeZoneInfo easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

DateTime?       next = expression.GetNextOccurrence(DateTime.UtcNow, easternTimeZone);
DateTimeOffset? next = expression.GetNextOccurrence(DateTimeOffset.UtcNow, easternTimeZone);
```

The return type tells you what happened. If you passed a DateTime object, the resulting time will be in UTC. If you used DateTimeOffset, the resulting object contains the correct offset, so the README warns not to forget to use it especially during DST transitions.

If you want local times, the guidance is to use DateTimeOffset throughout, because DateTime objects may be ambiguous during summer time transitions, and to read the local value from the DateTimeOffset.DateTime property.

## The compatibility table is the migration guide

Moving an existing cron schedule onto Cronos is where most of the risk lives, and the README has a compatibility section that states the differences without hedging.

Against Vixie Cron, there is one behavioural difference that matters. When both day-of-month and day-of-week are specified, Cronos uses the AND operator for matching, while Vixie Cron uses the OR operator for backward compatibility. A schedule that says the first of the month or any Monday means something different in each, and it changes the days a job actually runs.

Against Quartz.NET there are four differences. Cronos uses different, but more intuitive, daylight saving time handling logic, as in Vixie Cron. Full month names such as september aren't supported. The day-of-week field in Cronos has different values, where 0 and 7 stand for Sunday and 1 for Monday, as in Vixie Cron. And the year field is not supported.

NCrontab and CronNET are both listed as compatible, with no caveats.

The year field is the one to check silently. A Quartz schedule that names a year, for example a job that only runs in 2026, either fails to parse or loses its intent, and a schedule that was written with a year is often a schedule someone added to stop a job and forgot about.

The month name rule is the other. A Quartz expression that says september has to become a numeric month, so this is the kind of change that belongs in a test rather than in a code review.

## Installing and computing the first occurrence

Cronos is distributed as a NuGet package and can be installed from the official NuGet Gallery with the NuGet Package Manager Console:

```bash
PM> Install-Package Cronos
```

A typical first use is the parse-and-ask sequence shown earlier, and the thing to notice is the type of the argument you pass. Passing DateTime.UtcNow gives you a UTC result, and passing DateTimeOffset.UtcNow gives you a result with the right offset, which is the form you want in anything that schedules work.

The workflow the library is built for is therefore: parse the expression once, keep the CronExpression instance, and ask it for occurrences as your system advances. Because GetNextOccurrence is a pure calculation against a given instant, it can be called with any reference time, which is what makes it testable and what makes it usable for both a scheduler that asks every minute and a tool that wants to display the next ten runs.

Two rules of thumb come out of the README. Never pass DateTime.Now, and if you care about the local time of the occurrence, take the DateTimeOffset result and read its DateTime property rather than converting by hand.

For local-only calculations with no time zone involved, the README still steers you to DateTimeOffset, for the same reason: a local DateTime is ambiguous for part of the year. The library is not being fussy for the sake of it; the ambiguity is real and it is exactly the case that produces a job that runs twice.

## L, W, # and H are the extensions that make it usable in practice

The non-standard characters are the reason to choose this library over a regular expression parser, and each one solves a scheduling problem that standard cron cannot express.

L stands for the last day of the month, which is what you want for a billing run rather than a fixed day that does not exist in February. W stands for the nearest weekday to a given day of the month. The # character selects an occurrence within a month, so the third Tuesday is expressible in a single expression rather than by arithmetic on the day number.

H is the Jenkins style jitter, and it is a different kind of feature. Instead of a fixed minute, H means a value spread across the range, so a job scheduled with H does not run at the same instant on every host in a fleet. The README says it is inspired by Jenkins, and the reason it exists is contention: a hundred workers all starting at minute 0 is a thundering herd, and spreading the start is the difference between a job queue that absorbs the load and one that stalls.

The reversed ranges, 23-01 and DEC-FEB, are the other extension, and they read as a wrap-around: the range continues past the end of the range back to the beginning. That covers the night shift that crosses midnight, and the month that crosses the year boundary, without a special case in your code.

One caveat to carry with you. The month names that Quartz accepts are not accepted here, and the year field does not exist, so the migration from a Quartz schedule is where the syntax work is.

## MIT, a SonarCloud quality gate, and a version below 1.0

The licence is MIT, with the LICENSE file at the root of the repository, and the package is published on NuGet under the name Cronos.

The build and quality setup is visible in the README badges and the repository. There is an AppVeyor build status badge, a NuGet version badge, and a set of SonarCloud badges for the quality gate status, bugs and code smells. The repository carries appveyor.yml, a Directory.Build.props for shared build properties, a Cronos.snk strong name key, a psake-project.ps1 with the build script, a build.bat, a nuspecs directory, a benchmarks directory and a tests directory, plus a Cronos.sln.

A quality gate and a benchmark directory in a library this small is a reasonable signal about how the maintainers work, and it is also the reason the DST rules can be stated as guarantees rather than intentions: each of those three rules is a test case.

The version numbers are worth reading as a signal. The recent releases are 0.11.1 from 2025-08-12, 0.12.0 from 2026-04-08 and 0.13.0 from 2026-04-29, and the last push to the repository was on 2026-09-10. So the library is below 1.0, which in .NET convention means the API may still change between minor versions, and the pace of releases in 2026 was brisk.

For a library this small and this widely depended on, the risk is not in the code but in the schedule semantics, and the mitigation is to pin the package version and to write tests that assert the next occurrence for the specific expressions your system uses, including the ones that straddle a daylight saving boundary.

## Conclusion

Cronos fits a.NET team that needs to answer when a schedule next fires, especially one that has been bitten by a job silently skipped or duplicated across a daylight saving boundary. It does not fit a team looking for a job scheduler, because the README says in italics that the library does not include any task or job scheduler, and it does not fit a codebase that passes local DateTime values around, since those are rejected by design. Before you adopt it, read the compatibility table in the README, because the day-of-month and day-of-week matching differs from Vixie Cron and from Quartz.NET, and that difference changes when a job runs.

## FAQ

### Does Cronos include a job scheduler?

No. The README states in italics that this library doesn't include any task or job scheduler, it only works with Cron expressions. Cronos parses expressions and calculates the next and previous occurrence; the scheduling is left to the caller.

### How do I install Cronos in a .NET project?

It is distributed as a NuGet package and can be installed from the official NuGet Gallery with the NuGet Package Manager Console, using the command PM> Install-Package Cronos.

### Why does Cronos throw when I pass DateTime.Now?

You can only use DateTime with DateTimeKind.Utc specified, or DateTimeOffset, because a local DateTime may be ambiguous during DST transitions, and an exception is thrown if you attempt to use them.

### How does Cronos differ from Quartz.NET cron expressions?

Cronos uses different, but more intuitive, daylight saving time handling logic as in Vixie Cron, does not support full month names such as september, uses 0 and 7 for Sunday and 1 for Monday in the day-of-week field, and does not support the year field. It also uses AND rather than OR when both day-of-month and day-of-week are given, which is where Vixie Cron differs.

### Which non-standard cron syntax does Cronos support?

It supports L, W and # and their combinations, reversed ranges such as 23-01 and DEC-FEB, and schedule jitter through the H character inspired by Jenkins. It also supports the standard Cron format with optional seconds.

## Sources

- [HangfireIO/Cronos on GitHub](https://github.com/HangfireIO/Cronos)
- [Issues](https://github.com/HangfireIO/Cronos/issues)
- [License: MIT](https://github.com/HangfireIO/Cronos/blob/main/LICENSE)
- [README](https://github.com/HangfireIO/Cronos/blob/main/README.md)
- [Releases](https://github.com/HangfireIO/Cronos/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hangfireio-cronos
