Toolshed

A growing library of browser tools and deep technical guides for IT professionals.

← All guides

Diagnosing race conditions in shell scripts run by cron vs interactively

3 min read

A script works perfectly every time you run it by hand, then fails intermittently — or consistently — when cron runs it on schedule. This isn't a coincidence or "cron being flaky." Cron and an interactive shell start scripts in genuinely different conditions, and several of those differences create real race conditions.

Cause 1: the script assumes a working directory it doesn't get

Running a script by hand, you're usually sitting in a sensible directory — the project root, the script's own folder. Cron runs jobs from your home directory by default, regardless of where the script file lives. A script with relative paths (./data.csv, ../config.json) that works when run by hand can silently read or write the wrong file under cron — not always an outright crash, which makes it worse: a race condition against which file gets touched, not just whether the script runs.

# Fragile — depends on cwd, which differs between interactive and cron
python3 process.py --input data.csv

# Robust — resolve paths relative to the script's own location, not cwd
python3 "$(dirname "$0")/process.py" --input "$(dirname "$0")/data.csv"

Cause 2: a dependency isn't ready yet when cron fires

If a cron job depends on something else finishing first — a database migration, a file another job writes, a network mount being available — running it by hand after that dependency has already settled hides the race entirely. Cron firing at a fixed time doesn't guarantee the dependency finished; it only guarantees the clock hit that minute. Two jobs scheduled at the same time, where one depends on the other's output, is a genuine race condition that manual testing will essentially never reproduce, because you naturally run things in the right order by hand.

Fix: don't rely on timing coincidence. Either chain jobs explicitly (one job triggers the next on completion, rather than both being scheduled independently) or have the dependent job check for readiness and wait/retry rather than assuming.

Cause 3: environment variables that exist interactively but not under cron

Cron jobs run with a minimal environment — not your shell's PATH, not variables set in .bashrc or .profile, none of the environment your interactive terminal has built up over the session. A script that relies on an environment variable being set (an API key, a custom PATH entry, a locale setting) can behave differently or fail outright under cron even though it works fine when you run it by hand in a terminal where that variable is already set.

# In the crontab, explicitly set what the job needs — don't assume it's inherited
API_KEY=xxx PATH=/usr/local/bin:/usr/bin:/bin 0 * * * * /path/to/script.sh

The general pattern

Every one of these is the same underlying issue: an interactive shell has accumulated state (working directory, completed prior commands, environment variables) that a cron invocation starts fresh without. The fix in every case is the same principle — make the script's dependencies on directory, timing, and environment explicit in the script or the crontab entry itself, rather than relying on conditions that happen to be true when you test it by hand.

Sources: man 5 crontab (cron's execution environment and working directory defaults); general Unix process-environment behavior regarding inherited vs explicitly-set variables.