Cloud Explainers

How to Fix Python cron Jobs That Never Run

Python7 min videoPublished Nov 10, 2026

Your Python script works when you run it by hand, and does nothing from cron. In a real lab we put one script on a schedule and followed it: cron's own log shows it did start, its output was thrown away, and three things a terminal quietly provides were missing: the right Python, the right folder and your environment variables. Each failure is a real traceback, and each has a one-line fix.

Works by hand, not from cron

Your Python script works when you run it by hand. Put the same script in cron, and nothing happens. No output, no error, and no idea where to look. This dashboard should list two reports by now. It lists one: the run typed by hand.

Works by hand, not from cron: browser screenshot of run by hand, nothing from cron, one, not two

Same script, same machine. Ana rents out bikes, and her cron line should send the day's numbers on its own. Three things your terminal quietly hands a script are missing in cron, and the third still bites when everything looks fixed.

ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ python report.pyReport sent: 14 rentals, $280.50
ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ crontab -l40 13 * * * python3 /home/ana/bikes/report.py

Did cron even start it?

First: did cron even start the job? Cron is the Linux service that runs commands on a schedule, and this command prints its log. Guess first: is Ana's job in it? Yes. CMD means cron ran a command: hers, at thirteen forty, right on time. The next line explains the silence. No MTA installed, discarding output. An MTA is a mail program. Cron tried to mail Ana what the job printed, found no mail program, and threw it away.

ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ journalctl -u cron --since "3 minutes ago" --no-pagerNov 10 13:39:24 bikes systemd[1]: Started cron.service - Regular background program processing daemon.
Nov 10 13:39:24 bikes (cron)[83]: cron.service: Referenced but unset environment variable evaluates to an empty string: EXTRA_OPTS
Nov 10 13:39:24 bikes cron[83]: (CRON) INFO (pidfile fd = 3)
Nov 10 13:39:24 bikes cron[83]: (CRON) INFO (Running @reboot jobs)
Nov 10 13:40:01 bikes CRON[389]: pam_unix(cron:session): session opened for user ana(uid=1000) by ana(uid=0)
Nov 10 13:40:01 bikes CRON[390]: (ana) CMD (python3 /home/ana/bikes/report.py)
Nov 10 13:40:01 bikes CRON[389]: (CRON) info (No MTA installed, discarding output)
Nov 10 13:40:01 bikes CRON[389]: pam_unix(cron:session): session closed for user ana

So the quick fix comes before any guessing: keep the output. Ana keeps her cron lines in a text file, and the crontab command installs it. At the end of the line, two greater-than signs add whatever the job prints to a log file. And 2>&1 sends its error messages to that same file. One minute later, the log holds a real Python error: no module named requests. Requests is the library the script uses to reach the dashboard. And Ana's own run just used it.

ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ crontab crontab.txt
ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ crontab -l41 13 * * * python3 /home/ana/bikes/report.py >> /home/ana/bikes/cron.log 2>&1
ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ cat cron.logTraceback (most recent call last):
  File "/home/ana/bikes/report.py", line 4, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

Missing 1: which Python?

Think of cron as a house sitter working from your note. You make coffee every morning without thinking: your machine, your kitchen, your keys. The sitter starts at the front door with a note that says: make coffee. Which machine? Which room? Whose keys? Cron has the same three gaps.

A house sitter, working from your note: diagram of The sitter = cron, Your note = the cron line, Which machine?, Which room?, Whose keys?

Gap one: which Python? There are two on this machine. Ana's project has a virtual environment: a private folder with its own Python and its own libraries. Requests lives there. When she activates it, the word python means that one. Cron activates nothing. Its python3 is the system's Python, which has never heard of requests.

Two Pythons on one machine: diagram of Ana types python, .venv/bin/python, cron runs python3, /usr/bin/python3, the project's virtual environment

Check it yourself. The which command shows what a name will run. With the environment active, python is the project's. Deactivate, to leave the environment, and python3 is the system's. Run the script with that one, and Ana gets cron's exact error, by hand. That works for every gap: make your terminal as bare as cron, and the failure shows up in front of you.

ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ which python/home/ana/bikes/.venv/bin/python
ana@bikes: Ana's terminal
(.venv) ana@bikes:~/bikes$ deactivate
ana@bikes: Ana's terminal
which python3/usr/bin/python3
ana@bikes: Ana's terminal
python3 report.pyTraceback (most recent call last):
  File "/home/ana/bikes/report.py", line 4, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

The fix needs no activating. In the cron line, name the environment's own Python by its full path. Started that way, it finds its own libraries. Then the script, by its full path too.

In the cron line: the environment's own Python: diagram of /home/ana/bikes/.venv/bin/python, /home/ana/bikes = the project folder, /.venv/bin/ = its virtual environment, python = that environment's Python

Missing 2: which folder?

No such file: rentals.csv. That's the next run's error, and the file sits right beside the script. But the script asks for it by bare name, and a bare name means: in the folder I was started from. So, gap two. Which folder does cron start a job in? Your home folder. Ana added a second cron line that writes its folder to a file: slash home, slash ana. Not the project.

ana@bikes: Ana's terminal
tail -n 5 cron.logTraceback (most recent call last):
  File "/home/ana/bikes/report.py", line 6, in <module>
    with open("rentals.csv") as f:
         ^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'rentals.csv'
ana@bikes: Ana's terminal
crontab -l42 13 * * * /home/ana/bikes/.venv/bin/python /home/ana/bikes/report.py >> /home/ana/bikes/cron.log 2>&1
42 13 * * * pwd > /home/ana/cron-folder.txt; env > /home/ana/cron-env.txt
ana@bikes: Ana's terminal
cat ~/cron-folder.txt/home/ana

There are two fixes. The quick one: in the cron line, cd into the project first, then run. The lasting one: let the script find its own folder. Python gives every script a special name that holds its own location. Ana builds the file's path from that, and now it opens from anywhere, whoever starts it.

ana@bikes: Ana's terminal
grep -n "HERE" report.py7:HERE = Path(__file__).resolve().parent
9:with open(HERE / "rentals.csv") as f:

Missing 3: which variables?

One more minute, and here is gap three, the one that bites last. Key error. The dashboard wants a secret key, and the script reads it from an environment variable: a named value your shell hands to every program it starts. Ana set hers in.bashrc, a file her shell reads when a terminal opens. Cron opens no terminal, so it never reads that file. Ana's test line also saved what cron gave it: six variables. Her terminal has sixteen.

ana@bikes: Ana's terminal
tail -n 6 cron.logTraceback (most recent call last):
  File "/home/ana/bikes/report.py", line 13, in <module>
    key = os.environ["BIKES_API_KEY"]
          ~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "<frozen os>", line 685, in __getitem__
KeyError: 'BIKES_API_KEY'
ana@bikes: Ana's terminal
grep BIKES ~/.bashrcexport BIKES_API_KEY=bk_demo_4821
ana@bikes: Ana's terminal
cat ~/cron-env.txtHOME=/home/ana
LOGNAME=ana
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
LANG=C.UTF-8
SHELL=/bin/sh
PWD=/home/ana

So she copies her.bashrc line to the top of her cron file, and crontab refuses all of it. Bad command. Can't install. A crontab isn't a shell script, and export means nothing in it. Notice what that does: the old schedule stays in place. An edit that was never accepted is one real way a job never runs.

ana@bikes: Ana's terminal
cat crontab.txtexport BIKES_API_KEY=bk_demo_4821
43 13 * * * /home/ana/bikes/.venv/bin/python /home/ana/bikes/report.py >> /home/ana/bikes/cron.log 2>&1
ana@bikes: Ana's terminal
crontab crontab.txt"crontab.txt":1: bad command
errors in crontab file, can't install.
ana@bikes: Ana's terminal
crontab -l43 13 * * * /home/ana/bikes/.venv/bin/python /home/ana/bikes/report.py >> /home/ana/bikes/cron.log 2>&1

What a crontab does accept is a plain line at the top: name, equals, value. Every job below it gets that variable. She installs it, waits for the minute, and the last line of the log reads: report sent.

ana@bikes: Ana's terminal
cat crontab.txtBIKES_API_KEY=bk_demo_4821
44 13 * * * /home/ana/bikes/.venv/bin/python /home/ana/bikes/report.py >> /home/ana/bikes/cron.log 2>&1
ana@bikes: Ana's terminal
crontab crontab.txt
ana@bikes: Ana's terminal
tail -n 1 cron.logReport sent: 14 rentals, $280.50

And the dashboard lists a second report: thirteen forty-four, sent with nobody at the keyboard.

Missing 3: which variables?: browser screenshot of sent by cron

When it truly never starts

And when cron's log has no CMD line at all? Then the job truly never started, and there are three usual reasons. The cron service isn't running: systemctl, the tool that manages services, should say active. The line was never installed: crontab -l lists what cron really has. Or the clock: the date command shows the server's time, and on most servers that's UTC, the world's reference time, not your time zone.

ana@bikes: Ana's terminal
systemctl is-active cronactive
ana@bikes: Ana's terminal
dateTue Nov 10 13:44:05 UTC 2026

Scheduling Python on Windows instead? Task Scheduler leaves the same gaps. Point the task at the environment's own python.exe. Fill in the Start in box with the project folder. And run it through a small batch file that sends the output to a log. Every command and the lab files are linked in the description.

The same gaps in Windows Task Scheduler: comparison table of cron (Linux, macOS) vs Task Scheduler (Windows), Python, Folder, Output

Recap

So when a Python cron job never seems to run: read cron's log, then keep the job's output. Then give it what your terminal gave it for free: the right Python, the right folder, and its variables. Leave the house sitter a complete note.

A Python cron job that never seems to run: diagram of 1 Read cron's log, 2 Keep its output, The right Python, The right folder, Its variables, 3 what your terminal gave it for free

If your cron job finally has something to say for itself, subscribing keeps more small, real fixes of this kind coming.

Every error here came from a real cron job, run at a real minute in our lab. Thanks for watching Cloud Explainers.

Sources

Demos: a container on our own Mac standing in for a small shop computer; the bike shop, its dashboard and its key are made up. The Windows comparison comes from Microsoft's documentation. Watch the full video on YouTube.

More videos

Linux9 min

Linux Makefiles Will Finally Make Sense After This

Auth (OAuth, JWT)9 min

SAML vs OAuth vs OpenID Connect: What's the Difference?

Linux22 min

Linux Bash Scripting: Full Tutorial for Beginners

Networking10 min

How PXE Network Boot Works

Active Directory9 min

How Active Directory Works

Linux10 min

How to Fix a Linux systemd Service That Won't Start

Linux9 min

Linux Logs: syslog vs journald

Auth (OAuth, JWT)9 min

JWT vs Session Cookies: Which One Should You Use?

Docker10 min

Docker vs Docker Compose: Which One Should You Use?

Docker9 min

How to Fix Docker Compose Containers That Can't Talk to Each Other

Django (Python web framework)22 min

Django Full Tutorial for Beginners

Linux9 min

How Linux systemd Works

Networking8 min

How to Fix 'Windows Cannot Access' Network Share Errors (SMB)

APIs10 min

REST vs gRPC: Which One Should You Use?

Certificates & PKI (ADCS)9 min

How to Fix SSL Certificate Chain Errors (Missing Intermediate)

Python9 min

Every Python Concept for Beginners in 10 Minutes

Databases & SQL9 min

Every SQL Join Type in 10 Minutes

OpenRouter (one API for many AI models)9 min

Why OpenRouter Free Models Hit Their Limit So Fast

Gemini API & Google AI Studio9 min

Why Your Gemini API Free Tier Runs Out So Fast

OpenRouter (one API for many AI models)10 min

OpenRouter Will Finally Make Sense After This

Caching & Redis10 min

How Redis Caching Works

Gemini API & Google AI Studio10 min

Gemini API vs Vertex AI: Which One Should You Use?

Gemini API & Google AI Studio8 min

How to Fix a Leaked Gemini API Key

Go (Golang)9 min

Why Golang Uses Goroutines Instead of Threads

Gemini API & Google AI Studio9 min

How to Fix Gemini API Error 429: Resource Exhausted

Claude & the Claude Developer Platform10 min

Claude Token Pricing Will Finally Make Sense After This

Python11 min

Why Python Web Scraping Returns Nothing (and the Fix)

Python10 min

Python uv vs pip: Which One Should You Use?

Python22 min

Python Automation: Full Tutorial for Beginners

Auth (OAuth, JWT)9 min

How OAuth Login Works

Gemini API & Google AI Studio10 min

Google AI Studio Will Finally Make Sense After This

Docker10 min

What Happens When You Run a Docker Container

Claude & the Claude Developer Platform8 min

How to Fix Claude Code Billing Your API Key Instead of Your Plan

Auth (OAuth, JWT)9 min

Why Passkeys Can't Be Phished Like Passwords

PowerShell10 min

PowerShell vs Bash: Which One Should You Learn?

Linux10 min

The Linux Roadmap for Beginners

Django (Python web framework)9 min

Django vs FastAPI: Which One Should You Use?

Databases & SQL10 min

Why SQLite Is the Most Used Database in the World

Databases & SQL11 min

How a NoSQL Database Like MongoDB Works

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Git10 min

How Git Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index