All posts

Cron Job in Python: Setup, Silent Failures & Monitoring

September 14, 2026

Running a cron job in Python looks simple until the job that worked perfectly on your laptop does nothing on the server. Most tutorials stop at "add this line to crontab," but that's step one. The two problems developers actually hit in production are the silent environment and path failures unique to running Python under cron, and the fact that cron's exit code tells you nothing about whether your job actually did what it was supposed to do. This guide covers both, plus where APScheduler fits and how to close the visibility gap for good.

Running a Python Script from Cron: The Basic Pattern

Here's a minimal, correct crontab python script example that avoids the most common first-day mistakes:

*/15 * * * * /home/deploy/venv/bin/python /home/deploy/app/sync_orders.py >> /home/deploy/app/logs/sync_orders.log 2>&1

Three details matter here. First, both paths are absolute — cron runs with a minimal environment and no working directory assumption, so python sync_orders.py alone will almost certainly fail or run the wrong interpreter. Second, the interpreter path points directly into a virtualenv (venv/bin/python), not the system Python — this is how you use a virtualenv Python interpreter in crontab without needing to "activate" anything, since activation is just a shell convenience that sets PATH variables cron never sees. Third, output is redirected to a log file so you have somewhere to look when things go wrong.

This is the baseline. Every cron job python example you build on top of this should keep the same shape: absolute paths, explicit interpreter, explicit output capture. Once this line runs reliably, the real work — handling the failure modes cron doesn't warn you about — begins.

Why Python Jobs Fail Silently Under Cron (Even When the Script Works)

A script that runs fine manually but fails under cron is almost always an environment problem, not a code problem. Cron executes jobs with a stripped-down shell and a minimal set of environment variables — no PATH additions from your .bashrc, no API keys exported in your login shell, none of the context you take for granted at an interactive prompt. If your script calls os.environ["API_KEY"] and that variable was only ever set in your shell profile, cron job Python environment variables simply won't be there, and you'll get a KeyError with no obvious cause.

The second classic gotcha is the interpreter itself. If crontab calls plain python or python3 instead of the full path to your virtualenv's binary, it may resolve to a system Python that's missing every package your project depends on — leading to ModuleNotFoundError for packages you know are installed. This is why "python cron job not running" is often really "python cron job running with the wrong interpreter."

Relative file paths cause the same class of failure: cron doesn't run your script from the directory you expect, so open("data.csv") looks in the wrong place and throws a file-not-found error that never shows up because there's no captured stderr to catch it. Combine a missing environment variable, the wrong interpreter, and an uncaptured exception, and you get a job that appears to "just not run" — with zero clues why.

If your job also depends on parsing or generating cron schedule expressions programmatically, rather than just hardcoding them, see this deeper guide on scheduling with Croniter.

Logging Your Python Cron Job Properly

Once the environment issues are handled, you need visibility into what actually happened during each run. Python's built-in logging module is the right tool — reaching for print() statements alone won't survive redirection well and gives you no severity levels or timestamps:

import logging

logging.basicConfig(
    filename="/home/deploy/app/logs/sync_orders.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)

try:
    run_sync()
    logging.info("Sync completed successfully")
except Exception:
    logging.exception("Sync job failed")

Pair this with the >> logfile 2>&1 redirection in your crontab line so any traceback the script doesn't catch — a segfault, an unhandled import error — also lands in the file rather than vanishing. This solves how to log errors from a Python cron job, but it introduces a subtler problem: file logging alone is passive. Nobody tails a log file at 3 a.m. Logs pile up for weeks, the failure sits there in plain text, and the person who'd want to know finds out only when a customer complains. Logging output is necessary, but it's not the same as monitoring.

Crontab vs. APScheduler: Two Ways to Schedule Python

System crontab and APScheduler solve the same problem from opposite directions, and picking the wrong one for your architecture creates headaches later.

Crontab is simple and OS-managed: your script doesn't need to stay resident in memory, the operating system wakes it up on schedule, and a crashed run doesn't take anything else down with it. It's the right choice for short, independent scripts — a nightly export, a cleanup task, a sync job — that don't need to share state with a running application.

APScheduler runs inside your Python process using constructs like BackgroundScheduler and a CronTrigger that mimics cron syntax:

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger

scheduler = BackgroundScheduler()
scheduler.add_job(run_sync, CronTrigger(minute="*/15"))
scheduler.start()

This is the better fit when your scheduled job needs access to shared application state — a Flask or FastAPI app's in-memory cache, database connections already pooled, objects you don't want to reload from scratch every run. The trade-off is that APScheduler vs. cron isn't really a matter of "which is more powerful" but "does your job need a long-running process." If that process restarts, crashes, or gets redeployed, every scheduled job inside it disappears with it — cron jobs survive independently of any single process. For heavier distributed workloads with retries and queues across multiple workers, Celery is usually the next step up from either option. Check the official APScheduler repository for the current sync and async scheduling model before committing to it as your Python task scheduler.

The Real Gap: Knowing Whether the Job Actually Succeeded

Here's what neither crontab nor APScheduler gives you by default: proof that the job did its job. An exit code of 0 means the process didn't crash — it says nothing about whether the API call it made actually succeeded, whether the file it was supposed to write is non-empty, or whether the job hung indefinitely and never returned. Log files are reactive: they only help after someone thinks to check them. Neither tells you the moment a scheduled run goes missing entirely, which is exactly what happens when a server reboots, a systemd unit changes, or a deploy silently drops the crontab entry.

Some teams try MAILTO in crontab as a stopgap, but it has its own syntax pitfalls and rarely reaches anyone reliably — see why MAILTO fails so often if you've hit this wall already.

The fix is a heartbeat: your job pings an external endpoint when it starts and finishes, and if that ping doesn't arrive on schedule, you get alerted immediately instead of discovering the gap days later. That's what monitoring a Python cron job actually means — not watching a log file, but having something outside your server notice when a run doesn't happen. This is precisely what Cronevra is built for: HTTP-triggered check-ins that detect a failed or missing cron run and alert you the moment it happens, whether the job is scheduled with plain crontab or running inside APScheduler.

Once your Python job is scheduled, wiring in a check-in takes minutes. Add the ping, and a missed or failed run turns into an alert instead of a mystery three weeks later. See Cronevra's pricing to find a plan that fits your job volume.

Frequently Asked Questions

Why does my Python script run fine manually but fail when cron runs it?

Cron executes scripts with a minimal environment — no shell profile variables, no PATH additions, and a different working directory than you're used to interactively. The usual culprits are missing environment variables, relative file paths that no longer resolve, and cron calling the wrong Python interpreter entirely.

Do I need a virtual environment to run a Python cron job?

Not strictly, but you need consistency — cron must call the exact interpreter that has your project's dependencies installed. The safest approach is pointing crontab directly at your venv's Python binary (e.g., /home/deploy/venv/bin/python) rather than relying on activation, which cron never triggers.

Is APScheduler better than crontab for Python?

Neither is universally better — it depends on your architecture. APScheduler is better when your job needs to run inside a long-lived application and share its state; crontab is better for standalone scripts, since it doesn't depend on any single process staying alive.

How do I get email or Slack alerts when a Python cron job fails?

The traditional approach is crontab's MAILTO variable, but it's notoriously unreliable due to mail server configuration issues. A more dependable method is an HTTP-based monitoring service like Cronevra, which alerts you directly when a scheduled job fails or doesn't check in on time.

Can I run a Python cron job every minute?

Yes — set the minute field to * in your crontab schedule to run every minute, or use */N for every N minutes. Keep in mind that very frequent jobs make it even more important to have monitoring, since a silent failure can go unnoticed for many runs in a short time.

How do I debug a Python cron job that isn't running at all?

Start by checking that the crontab entry uses absolute paths for both the interpreter and the script, then confirm the job actually appears in the crontab with crontab -l. Next, verify redirected output in your log file for tracebacks, and test the exact command cron would run directly in a non-interactive shell to reproduce the environment.