154. Cron + Schedule (App V1.2)
RAM Manager v1.2 — automated scheduling with cron
154. Cron + Schedule (App V1.2)
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 -rCron 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)
│ │ │ │ │
* * * * * commandCommon 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
python3pointed to the system Python and their dependencies weren’t installed there.sys.executableis 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 thisWithout 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 ⚖️
| Cron | schedule | |
|---|---|---|
| Survives reboot? | Yes | No |
| Runs when script exits? | Yes | No |
| Granularity | 1 minute | 1 second |
| Good for | Production | Development, demos |
| Setup | crontab -e | Just 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: sys.executable returns the full absolute path to the current Python interpreter — like /Users/aryan/.venv/bin/python. This path is set by the OS when Python starts, not by anything in your script. It’s one of the few values in Python that you can always trust to be correct, because the OS itself set it. There’s no way to fake it from within Python.
⚠️ Watch out: job.minute.every(5) and job.setall('*/5 * * * *') both schedule every 5 minutes — but they’re different APIs on the same object. If you call job.minute.every(5) and then job.setall(...), the second call overwrites the first. Pick one. The setall() method takes the full cron expression string, while job.minute.every(N) is the fluent API for setting just one field. Both produce the same result in the crontab.
🤔 Think about it: The broken code uses 'python3' as the command in the cron job. On a Mac with Homebrew, python3 might be Python 3.9. On a server, it might be 3.11. In your virtualenv, it might point to the system Python entirely. How do you verify this? Run which python3 and sys.executable in your venv and compare the outputs. They’re often different. What would happen to your cron job if they differ and your dependencies are only installed in the venv?
⏰ Aryan wants the RAM manager to run automatically every 5 minutes and email a report if RAM exceeds 90%. He tries to install a cron job but uses a hardcoded python path instead of sys.executable, mixes up the cron field order, and for the in-process scheduler he busy-waits without sleeping.
Learning objectives
- Use sys.executable to reference the current Python interpreter
- Understand cron field order (minute hour dom month dow)
- Install/remove cron jobs with python-crontab
- Use schedule library with time.sleep(1) in the event loop
Key concepts
- sys.executable — path to current Python interpreter
- job.minute.every(N) — cron scheduling at minute granularity
- comment=‘tag’ — label cron jobs for easy removal
- cron.remove_all(comment=‘tag’) — clean removal
- schedule.run_pending() + time.sleep(1) — cooperative event loop
Try it
Concept detail
App v1.2 — Automated scheduling
# Install cron job (runs every 5 min)
$ python ram_manager.py install
Cron job installed: every 5 minutes
# See what was installed
$ crontab -l
*/5 * * * * /usr/bin/python3 /home/aryan/ram_manager.py monitor >> /tmp/ram_manager.log 2>&1
# Remove it
$ python ram_manager.py remove
Removed 1 cron job(s)Cron field order
minute hour day-of-month month day-of-week
*/5 * * * *In-process scheduler
import schedule, time
schedule.every(5).minutes.do(check_ram)
while True:
schedule.run_pending()
time.sleep(1) # ← NEVER skip thissys.executable vs ‘python3’
# Bad — wrong Python if using a virtualenv
command = f'python3 {script}'
# Good — always uses the current interpreter
command = f'{sys.executable} {script}'Solution
import sys
import time
import subprocess
from pathlib import Path
from crontab import CronTab
SCRIPT_PATH = Path(__file__).resolve()
def take_snapshot(n: int = 5) -> dict:
import psutil
from datetime import datetime
mem = psutil.virtual_memory()
processes = []
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
rss_mb = proc.info['memory_info'].rss / 1e6
processes.append({'name': proc.info['name'], 'pid': proc.info['pid'], 'rss_mb': rss_mb})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return {
'percent': mem.percent,
'used_gb': mem.used / 1e9,
'total_gb': mem.total / 1e9,
'processes': sorted(processes, key=lambda p: p['rss_mb'], reverse=True)[:n],
'timestamp': datetime.now().isoformat(),
}
def install_cron_job(interval_minutes: int = 5) -> None:
cron = CronTab(user=True)
cron.remove_all(comment='ram_manager')
job = cron.new(
command=f'{sys.executable} {SCRIPT_PATH} monitor >> /tmp/ram_manager.log 2>&1',
comment='ram_manager',
)
job.minute.every(interval_minutes) # correct field: .minute not .second
cron.write()
print(f'Cron job installed: every {interval_minutes} minutes')
def remove_cron_job() -> None:
cron = CronTab(user=True)
removed = cron.remove_all(comment='ram_manager')
cron.write()
print(f'Removed {removed} cron job(s)')
def list_cron_jobs() -> list[str]:
cron = CronTab(user=True)
return [str(job) for job in cron if job.comment == 'ram_manager']
def run_scheduler(interval_seconds: int = 300) -> None:
import schedule
schedule.every(interval_seconds).seconds.do(lambda: print(take_snapshot()['percent']))
while True:
schedule.run_pending()
time.sleep(1) # yield to OS between checks
if __name__ == '__main__':
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'install':
install_cron_job()
elif len(sys.argv) > 1 and sys.argv[1] == 'remove':
remove_cron_job()
else:
snapshot = take_snapshot()
print(f"RAM: {snapshot['percent']:.1f}%")Tests
import sys
import inspect
from unittest.mock import patch, MagicMock, call
def test_install_cron_uses_sys_executable():
src = inspect.getsource(install_cron_job)
assert 'sys.executable' in src, \
'Use sys.executable not hardcoded "python3" — paths differ per virtualenv'
def test_install_cron_uses_minute_field():
src = inspect.getsource(install_cron_job)
assert '.minute.' in src or 'minute.every' in src, \
'Use job.minute.every(N) — not .second or .seconds'
assert '.second' not in src, \
'cron has no seconds field — use job.minute.every(N)'
def test_install_cron_sets_comment():
src = inspect.getsource(install_cron_job)
assert 'ram_manager' in src, \
'Tag the job with comment="ram_manager" so it can be removed later'
def test_remove_cron_filters_by_comment():
src = inspect.getsource(remove_cron_job)
assert 'ram_manager' in src, \
'remove_all(comment="ram_manager") removes only our jobs'
def test_list_cron_jobs_filters_by_comment():
src = inspect.getsource(list_cron_jobs)
assert 'ram_manager' in src, \
'Filter cron jobs by comment="ram_manager"'
def test_run_scheduler_sleeps():
src = inspect.getsource(run_scheduler)
assert 'time.sleep' in src, \
'Call time.sleep(1) inside the while loop to avoid busy-waiting'
def test_run_scheduler_calls_run_pending():
src = inspect.getsource(run_scheduler)
assert 'run_pending' in src, \
'Call schedule.run_pending() inside the loop'