Three conversions that quietly break in production: cron, SQL dialects, and time zones
Every team has a small set of conversions that nobody owns. They get pasted into a scratch file, run once, and forgotten — until the one month a year when they are wrong.
I collected three of them into browser tools because I kept re-deriving them, and each one has a failure mode that only shows up at a specific time of year or on a specific engine. The failure modes are more interesting than the tools, so here they are.
1. Cron: the expression is right and the job still doesn't run
0 2 * * * means "2am every day". In a timezone with daylight saving, it means "2am every day except the day the clocks jump", because 02:00 does not exist on that date. Many schedulers skip the run entirely; some run it twice on the way back.
The mistake is treating a cron expression as a time. It is a local calendar rule, and local calendars have holes and duplicates:
- Spring forward: 02:00–03:00 never happens. A job pinned to 02:30 has no valid instant.
- Fall back: 01:00–02:00 happens twice. A job pinned to 01:30 has two valid instants.
So a useful cron explainer has to do two things beyond translation:
- Resolve the expression against a real timezone, not against UTC arithmetic. "Next 5 runs" is only useful in the timezone the operator lives in, with DST rules applied to each occurrence independently.
- Show the hole. If the next occurrence lands in a spring-forward gap, say so, rather than silently skipping to the day after — that is the difference between a bug report and a mystery.
The other thing worth surfacing is the count of runs: */15 in the minute field is 96 runs a day, and 0 0 1 * * is 12 a year. Half of "why is our queue flooded" tickets are somebody reading a cron as "every hour".
I built a cron explainer that prints the next runs in your own zone with DST applied, and flags the DST edge case explicitly: Cron Expression Explainer.
2. SQL dialects: silent disagreement is worse than a syntax error
Every ORM promises dialect independence, and every project with more than one database eventually hits the seams. The tricky ones are not the syntax errors — those you find in CI. The tricky ones are the constructs that are valid in both dialects and mean different things:
| Construct | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
"double quotes" |
string literal (unless ANSI_QUOTES) |
identifier | identifier |
LIMIT 10, 20 |
offset 10, count 20 | syntax error | offset-comma supported |
TRUE / FALSE
|
tinyint 1/0 | real boolean | 1/0 stored as integer |
INSERT ... ON DUPLICATE KEY |
native upsert | needs ON CONFLICT
|
ON CONFLICT or INSERT OR REPLACE
|
| Implicit string→number cast | permissive | strict, errors | permissive |
The "double quotes" row is the one that gets people. A query written against MySQL that quotes a string with double quotes will, after a migration, be interpreted as an identifier — and depending on the driver's quoting behaviour, it can fail loudly or return something plausible. Plausible is the dangerous outcome.
That is why a converter that guesses is worse than one that refuses. When I built the SQL formatter/dialect converter, the rule I settled on was: translate what is provably equivalent, list every change made, and flag anything that cannot be translated safely instead of approximating it. A converter that hands you SQL and says "3 constructs were rewritten, 1 could not be translated" is usable in a migration review. One that silently produces something that runs is a trap.
3. Time zones: offsets are not time zones
The classic bug: someone stores a timestamp with an offset, or worse, converts to a fixed offset once and stores that. Then a date six months later is off by an hour, because the offset was a property of one moment, not of the zone.
Two facts that make this concrete:
-
An offset is a snapshot; a zone is a rule set.
2026-03-29T01:30+01:00tells you one instant.Europe/Londontells you how to interpret every wall-clock time in that zone — differently before and after the transition. -
Wall-clock times can be missing or duplicated. Same holes as cron. If a user types
2026-03-29 01:30in London, that time does not exist; a naive parser will happily produce an instant an hour off, and the confirmation email will say the wrong thing.
The practical rules I keep re-learning:
- Store UTC instants plus the IANA zone name if you need the local wall clock later. Never store a derived offset as the source of truth.
- When parsing user input, treat "this time does not exist" and "this time is ambiguous" as validation errors you must decide about — not as parser edge cases. Pick a policy (shift forward / pick the first occurrence / reject), and put it in the code where a reviewer can see it.
- When displaying, convert at the edge. Formatting in the database is where zone rules go to die.
The time zone converter exists because I wanted to see the failure rather than have it explained: enter a wall-clock time and a zone, and if that time falls in a gap or a duplicate, it says so instead of picking silently.
Why these live in the browser
All three are pure functions: text in, text out. There is no reason for a server to be involved, and a real cost if one is — anything pasted into a "quick converter" is often a production fragment, a customer list, or a connection string.
So the constraint I set was: the input never leaves the page. No upload, no account, no analytics on the content. It simplifies the security story to nothing and removes the temptation to log request bodies "for debugging".
The trade-offs are honest ones: no server means all the parsing happens in JavaScript on the user's machine, so heavy inputs are limited by their browser, and there is no API for scripting. For a pasted cron expression or a 200-line SQL file, that is the right trade.
The other two conversions in the same family — JSON/YAML/TOML with errors pinned to line and column, and IP subnet with IPv6 — follow the same rule: show the failure precisely, do not paper over it.
If you have a fourth conversion in this category — one where being silently plausible is worse than erroring — I would like to hear which one, because that is the list I am building next.
Top comments (0)