All posts
5 min readBy Anggadevops / automation

The cron job I avoided for five years

I ran the same script by hand for years because cron looked like a foreign language. It took five minutes to learn. Here is the version of the explanation I wish someone had given me.

For about five years, every morning at 9am, I ran a script.

Not because I had to be there. Not because anything technical required a human in the loop. I ran it because I had never learned how to write a cron job, and the first time I'd looked at the syntax, my brain had quietly filed it under "things sysadmins do." Five asterisks in a row. Numbers between them. It looked, to my eyes at the time, like an error message.

So instead I opened a terminal. I ran the script. I closed the terminal. I went on with my day. And I did this, in some form or another, on something close to a thousand mornings before I bothered to learn what the asterisks meant.

This post is the explanation I should have read on morning two.

The thing I was afraid of

Here is what scared me. Picture an old crontab line in a tutorial:

*/15 9-17 * * 1-5  /usr/local/bin/sync-orders.sh

I would look at that and feel my shoulders rise. The slash. The dash. The pile of asterisks. It looked like it could mean anything. It looked like it might require me to know what "user crontab" meant, or "system crontab," or which user owns /usr/local/bin/. I assumed there were layers. I assumed I'd need to learn the layers before I could write a single line.

There are no layers. There is one sentence in five parts.

The five parts

Read left to right. Each column is one number, one list, or one star.

*  *  *  *  *  command-to-run
|  |  |  |  |
|  |  |  |  +-- day of week  (0-6, Sunday is 0)
|  |  |  +----- month        (1-12)
|  |  +-------- day of month (1-31)
|  +----------- hour         (0-23)
+-------------- minute       (0-59)

That's the whole language.

A star in any position means "any value for this column." So * * * * * means "every minute of every hour of every day," which is the most aggressive schedule cron offers. A number means "exactly this." 0 9 * * * means "minute 0 of hour 9, any day, any month, any weekday," which is shorthand for "9:00 AM every day."

Three more pieces of grammar and you know everything:

  • */N means "every N." So */15 * * * * runs every fifteen minutes.
  • A-B means "any value in that range." 0 9-17 * * * runs hourly between 9am and 5pm.
  • A,B,C means "this list of values." 0 9,12,18 * * * runs at 9am, noon, and 6pm.

That's it. There isn't a sixth thing. The sentence I was scared of for half a decade had a grammar smaller than CSS selectors.

A small story about asterisks

The morning I finally learned this, I had been on call for a release. Something needed to run at 2am every night to clean up old session data. We had a person on the team doing it. Not because it was hard, but because nobody had felt like figuring out cron and "we can fix it later" had won every retrospective for six months.

I typed crontab -e for the first time. My editor opened. I added one line:

0 2 * * *  /opt/app/bin/cleanup-sessions.sh

I saved. I closed the file. Nothing crashed. The next morning the cleanup had run on its own.

I sat with my coffee for a long minute. Five years. I had been doing this for five years. The wall was a fence.

Where you actually write these today

If you're not running a long-lived Linux box, you might never touch crontab -e in your career. The five-asterisk schedule has been wrapped, dressed up, and rebranded by every modern platform, and the wrapper you reach for depends on where your code lives.

Linux crontab. crontab -e on any Unix machine. The classic. Still the right answer for self-hosted services, VPS work, and personal home servers.

Vercel Cron. A crons block in vercel.json pointing at a route in your app. The Hobby tier runs once-per-day jobs for free, which is exactly what most blogs and side projects need. Pro tier unlocks per-minute precision.

{
  "crons": [
    { "path": "/api/rebuild", "schedule": "0 6 * * *" }
  ]
}

GitHub Actions. A workflow with a schedule trigger. Free for public repos, and a great fit for "ping this URL once a day" or "run my tests every Sunday." The schedule field uses the exact same five-asterisk syntax.

on:
  schedule:
    - cron: "0 6 * * *"

App-level libraries. node-cron, BullMQ, Python's APScheduler, Rails's whenever. Pick this when the schedule lives inside your application process, usually because it depends on data the app already has loaded.

Same syntax everywhere. Five asterisks. One command. The wrapper differs; the language doesn't.

The two things that will bite you

Almost every engineer who picks up cron stubs their toe on the same two things in their first week. Here they are upfront.

Timezone is almost never your timezone. Most cron systems default to UTC. If you schedule 0 9 * * * from Indonesia expecting "9am my time," your job runs at 4pm. Linux crontab uses the system timezone (usually UTC on cloud Linux boxes). Vercel Cron uses UTC. GitHub Actions uses UTC. Pretty much everything in the cloud uses UTC. Set the schedule in UTC and convert in your head, or set the system timezone explicitly.

A failed run is just a failed run. Cron does not retry. If your job dies halfway through, the next scheduled invocation is the next thing that happens. So always pipe output somewhere you'll see it, and add a "did this thing actually run" check on something that matters. The classic mistake is to write a cron job, watch it work twice, walk away, and discover three weeks later that it has been silently failing since week one.

A third thing while we're here: jobs can overlap if they run longer than their interval. A daily job is safe. A "every minute" job that takes 90 seconds will fire on top of its previous run. Use a lock file or a queue. This is not common, but it ruins a Saturday when it happens.

What I do now

The lesson from those five years is simple. If I find myself doing something on a schedule twice, I write a cron job for it. The whole evaluation takes about three minutes. I open crontab.guru if the schedule is interesting, I write the line, I commit it somewhere visible, and I move on. The script that used to ruin my morning routine is now a thing my computer does while I'm asleep.

The five-minute tax I paid that one Friday has refunded itself a hundred times over. It is genuinely the highest-leverage piece of foundational knowledge I ever spent an afternoon on. Higher than git rebase. Higher than vim motions. Higher than learning regex.

If you have a script you run by hand, today is a good day for it.