← Home

153. Cron + Python Crontab + Schedule

Scheduling recurring tasks with cron and Python

153. Cron + Python Crontab + Schedule

He wants the monitor to run automatically every 5 minutes.

πŸ€” Socratic question: Your phone sends you app notifications, your bank runs interest calculations overnight, your streaming service generates recommendations while you sleep. None of this has a human clicking β€œrun.” Who clicks run? Nobody. That’s what schedulers are for.

πŸ›οΈ History: Cron was written by Ken Thompson (co-inventor of Unix, C, Go, and UTF-8) in 1975. The name comes from the Greek Chronos, god of time. Its cron syntax hasn’t changed in 50 years. The exact same * * * * * format that Thompson wrote in 1975 is what you’ll use today. Your bank uses it. NASA uses it. And now, you will too.

Cron: The System Scheduler πŸ“…

Cron runs commands on a schedule. It’s been in Unix since 1975.

# Edit your crontab
crontab -e

# List your cron jobs
crontab -l

# Remove all cron jobs (be careful)
crontab -r

Cron jobs survive reboots. They run even when you’re not at the keyboard.

πŸ’‘ Real-world: Your bank calculates interest every night at midnight β€” that’s a cron job. Twitter/X generates β€œtrending topics” every 15 minutes β€” cron job. Instagram sends you β€œyou have new activity” emails β€” cron job. Most of the internet runs on 50-year-old scheduling code.

Cron Field Order πŸ—ΊοΈ

The hardest part: getting the field order right.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ minute        (0–59)
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ hour          (0–23)
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€ day of month  (1–31)
β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€ month         (1–12)
β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€ day of week   (0–6, Sun=0)
β”‚ β”‚ β”‚ β”‚ β”‚
* * * * *  command

Common patterns:

*/5 * * * *     every 5 minutes
0 9 * * *       every day at 9:00 AM
0 9 * * 1-5     weekdays at 9:00 AM
0 0 1 * *       first day of every month

πŸ“Ί YouTube search: β€œcron job explained in 5 minutes” β€” every video uses the same mnemonic: β€œMinute Hour DayOfMonth Month DayOfWeek”. You’ll never forget it.

Installing a Cron Job from Python 🐍

import sys
from crontab import CronTab

cron = CronTab(user=True)
cron.remove_all(comment='ram_manager')   # remove old jobs first

job = cron.new(
    command=f'{sys.executable} /path/to/ram_manager.py monitor',
    comment='ram_manager',   # tag for easy removal later
)
job.minute.every(5)   # sets */5 * * * *
cron.write()

Critical: use sys.executable, not 'python3'. In a virtualenv, python3 may point to the system Python, not your venv. sys.executable always points to the exact interpreter currently running.

πŸ’€ War story: Developers have deployed cron jobs that ran with the wrong Python and silently failed for weeks because python3 pointed to the system Python and their dependencies weren’t installed there. sys.executable is the correct answer. Always.

In-Process Scheduling with schedule ⏱️

For development and testing, you can run the scheduler inside Python:

import schedule, time

def check():
    snapshot = take_snapshot()
    if snapshot['percent'] > 80:
        print(f"⚠️  RAM: {snapshot['percent']:.1f}%")

schedule.every(5).minutes.do(check)

while True:
    schedule.run_pending()
    time.sleep(1)   # NEVER skip this

Without time.sleep(1), the while loop spins at full CPU speed doing nothing. One line. Huge impact on battery and CPU temperature.

🀯 Fun fact: A busy-wait loop (while True with no sleep) will peg your CPU at 100% and drain your laptop battery in under an hour. Python’s GIL doesn’t save you here. One time.sleep(1) fixes it. The difference between 0% CPU and 100% CPU is one line.

Cron vs In-Process Scheduler βš–οΈ

Cronschedule
Survives reboot?YesNo
Runs when script exits?YesNo
Granularity1 minute1 second
Good forProductionDevelopment, demos
Setupcrontab -eJust Python

For a real tool: use cron. For demos and testing: schedule is fine.

πŸ’‘ Real-world: AWS Lambda, Google Cloud Functions, and Azure Functions are all β€œserverless cron” β€” you write a function and tell the cloud when to run it. The concept is identical to cron, just with auto-scaling and billing per execution instead of a single machine.

πŸ’‘ Fun fact: The crontab.guru website (crontab.guru) was created by a developer who got tired of debugging cron expressions. It processes over 1 million cron expression lookups per day β€” proof that even experienced developers confuse the field order constantly. There’s no shame in looking it up every time.

⚠️ Watch out: schedule.every(300).seconds schedules every 300 seconds (5 minutes). But schedule.every(5).minutes does the same thing and reads much better. The subtle danger is mixing up .seconds and .minutes β€” schedule.every(5).seconds runs every 5 seconds, not every 5 minutes. Always use the most human-readable unit so bugs are obvious at a glance.

πŸ€” Think about it: Why does install_cron_job() call cron.remove_all(comment='ram_manager') before adding a new job? What happens if you call install_cron_job() twice without removing first? How is this pattern called? (Hint: it’s called β€œidempotency” β€” running an operation multiple times produces the same result as running it once. This is a critical property for deployment scripts, cron installers, and infrastructure as code.)


⏰ Aryan wants the RAM manager to run automatically every 5 minutes without him having to remember to start it. He confuses the cron field order, schedules a Python script using the wrong Python path, and doesn’t know how to list or remove existing cron jobs.

Learning objectives

  • Know cron field order (minute hour day month weekday)
  • Use sys.executable for the right Python path in cron jobs
  • Tag cron jobs with comment= for easy management
  • Use schedule library with time.sleep() for Python-only scheduling
  • Know when to use cron vs schedule

Key concepts

  • cron minute field β€” */5 = every 5 minutes
  • sys.executable β€” path to current Python interpreter
  • CronTab(user=True) β€” manage user’s crontab from Python
  • schedule.every(N).minutes.do(fn) β€” Python scheduler
  • time.sleep() in loop β€” avoid busy-wait
  • cron.remove_all(comment=) β€” idempotent job management

Try it

Concept detail

Scheduling with Cron and Python

Cron syntax (field order matters!)

* * * * *  command
β”‚ β”‚ β”‚ β”‚ └── day of week (0-7, 0=Sun)
β”‚ β”‚ β”‚ └──── month (1-12)
β”‚ β”‚ └────── day of month (1-31)
β”‚ └──────── hour (0-23)
└────────── minute (0-59)
*/5 * * * *        # every 5 minutes
0 * * * *          # every hour on the hour
0 9 * * 1-5        # 9am weekdays
0 0 1 * *          # midnight on 1st of month
30 8 * * *         # 8:30am every day

Shell: manage cron jobs

crontab -l         # list current jobs
crontab -e         # edit (opens in $EDITOR)
crontab -r         # remove all jobs (careful!)

# Add a job manually:
# */5 * * * * /usr/local/bin/python3 /home/aryan/ram_manager.py

python-crontab β€” manage cron from Python

from crontab import CronTab
import sys

cron = CronTab(user=True)   # current user's crontab

# Add a job
job = cron.new(
    command=f'{sys.executable} /path/to/ram_manager.py',
    comment='ram-manager'
)
job.setall('*/5 * * * *')   # every 5 minutes
cron.write()

# List, find, remove
for job in cron:
    print(job)

cron.remove_all(comment='ram-manager')
cron.write()

schedule library β€” pure Python scheduler

import schedule, time

def check_ram():
    print('Checking RAM...')

schedule.every(5).minutes.do(check_ram)
schedule.every().hour.do(check_ram)
schedule.every().day.at('09:00').do(check_ram)

while True:
    schedule.run_pending()
    time.sleep(30)   # check every 30s

Cron vs schedule library

cronschedule
Survives rebootyesno (process must run)
Setupone-time crontab editloop in Python
Good forproduction serversdev/simple scripts

Solution

import sys
from crontab import CronTab
import schedule
import time

CRON_COMMENT = 'ram-manager'

def install_cron_job(script_path: str, interval_minutes: int = 5) -> None:
    cron = CronTab(user=True)
    # Remove existing ram-manager jobs first (idempotent)
    cron.remove_all(comment=CRON_COMMENT)

    # Use the current Python interpreter for the right venv
    python = sys.executable
    job = cron.new(command=f'{python} {script_path}', comment=CRON_COMMENT)

    # Cron field order: minute hour day-of-month month day-of-week
    job.setall(f'*/{interval_minutes} * * * *')   # every N minutes

    cron.write()
    print(f'Installed: {job}')

def remove_cron_job() -> int:
    cron = CronTab(user=True)
    removed = cron.remove_all(comment=CRON_COMMENT)
    cron.write()
    return removed

def run_on_schedule(check_fn, interval_minutes: int = 5) -> None:
    schedule.every(interval_minutes).minutes.do(check_fn)
    print(f'Running every {interval_minutes} minutes. Ctrl+C to stop.')
    while True:
        schedule.run_pending()
        time.sleep(30)   # check every 30 seconds, not busy-wait

def list_ram_manager_jobs() -> list[str]:
    cron = CronTab(user=True)
    return [str(job) for job in cron if job.comment == CRON_COMMENT]

Tests

import time
import pytest
import schedule as sched

def test_run_on_schedule_uses_sleep():
    import inspect
    src = inspect.getsource(run_on_schedule)
    assert 'time.sleep' in src, 'Must sleep in the loop β€” no busy-wait'

def test_run_on_schedule_uses_minutes():
    import inspect
    src = inspect.getsource(run_on_schedule)
    assert '.minutes' in src, 'Use schedule.every(N).minutes not .seconds'

def test_list_ram_manager_jobs_filters_by_comment():
    import inspect
    src = inspect.getsource(list_ram_manager_jobs)
    assert 'comment' in src or 'CRON_COMMENT' in src, (
        'Must filter by comment β€” do not return ALL cron jobs'
    )

def test_install_cron_job_uses_sys_executable():
    import inspect
    src = inspect.getsource(install_cron_job)
    assert 'sys.executable' in src, (
        'Use sys.executable to get the right Python interpreter path'
    )

def test_install_cron_job_sets_minute_interval():
    import inspect
    src = inspect.getsource(install_cron_job)
    # The expression */N should appear in the minute field (first field)
    assert '*/interval_minutes' in src or '*/{interval_minutes}' in src or 'setall' in src

def test_schedule_callback_called():
    calls = []
    sched.clear()
    sched.every(1).seconds.do(lambda: calls.append(1))
    time.sleep(1.1)
    sched.run_pending()
    assert len(calls) >= 1

Resources