Back to Blog
Python

Scheduling Python Jobs Every Minute, Hour, and Day

python schedule periodic jobs every minute hour and day: Compare time.sleep, the schedule library, and APScheduler for running Python jobs at minute, hour, and day int...

schedulingAPSchedulercronautomationbackground-jobs
Illustration of a Python scheduler dispatching jobs to clocks representing minute, hour, and day intervals

python schedule periodic jobs every minute hour and day requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Running a function at a fixed cadence is one of the most common automation needs in Python. Polling an API, rotating logs, syncing data, or sending a daily report all require code that executes every minute, every hour, or at a specific time of day. The right approach depends on how much control you need over timing, overlap, and failure recovery. This article covers the practical options for scheduling periodic jobs in Python at minute, hour, and day intervals, and explains when each one is the right fit.

A Loop with time.sleep() — The Minimal Scheduler

The simplest way to run a job repeatedly is a while loop with time.sleep():

import time def collect_metrics(): print("collecting metrics") while True: collect_metrics() time.sleep(60)

This runs collect_metrics(), then pauses for 60 seconds before the next call. For an hourly job, replace 60 with 3600. The approach works for one-off scripts where the process is expected to run for a bounded period and the exact interval does not matter.

The problem is drift. The real interval is execution time plus sleep time. If collect_metrics() takes five seconds, the job actually runs every 65 seconds, not every 60. Over a day, that accumulates to roughly 12 minutes of delay. If the job throws an exception, the loop exits entirely, so the function must catch its own errors:

while True: try: collect_metrics() except Exception: log_exception() time.sleep(60)

There is also no catch-up behavior. If the process is suspended or the machine sleeps, the job simply does not run during that window, and the loop resumes from wherever it stopped. For a daily job at a specific wall-clock time, you would need to compute the sleep duration until the next target, which quickly becomes fragile across timezones and daylight saving changes.

The schedule Library for Readable Intervals

The schedule library translates interval descriptions into a job queue with a much more readable API:

import schedule import time def collect_metrics(): print("collecting metrics") schedule.every().minute.do(collect_metrics) schedule.every().hour.do(collect_metrics) schedule.every().day.at("09:30").do(collect_metrics) while True: schedule.run_pending() time.sleep(1)

Each every() call registers a job. run_pending() checks which jobs are due and executes them in the calling thread. The time.sleep(1) keeps the loop responsive; you can reduce it to 0.1 if you need sub-second scheduling precision, though that increases CPU wakeups.

Jobs run sequentially in the same thread. If collect_metrics() takes two minutes, the next minute-based run is skipped until the loop reaches the next due time. The library does not create threads or catch exceptions, so a failing job stops the loop unless you wrap the job body in a try/except.

The library also supports day-of-week schedules:

schedule.every().monday.at("09:00").do(collect_metrics) schedule.every().wednesday.at("18:30").do(send_report)

The schedule library is a good fit for small internal tools where the job set is static, the process runs continuously, and a missed run is acceptable. It does not persist job state, so a restart loses all registrations and any missed runs are simply gone.

APScheduler for Cron-Style Control

APScheduler provides a more robust scheduling layer with cron-like triggers, concurrency control, and optional job persistence:

from apscheduler.schedulers.blocking import BlockingScheduler def collect_metrics(): print("collecting metrics") scheduler = BlockingScheduler() scheduler.add_job(collect_metrics, "interval", minutes=1) scheduler.add_job(collect_metrics, "interval", hours=1) scheduler.add_job(collect_metrics, "cron", hour=9, minute=30) scheduler.start()

Two trigger types cover the minute, hour, and day cases directly:

  • interval runs the job every N seconds, minutes, or hours, measured from the previous start time.
  • cron runs the job at specific calendar times, matching system cron semantics.

BlockingScheduler occupies the main thread, which is fine for a dedicated scheduler process. If you are embedding scheduling in an existing application, BackgroundScheduler runs the scheduler in a background thread so the main thread can continue serving requests.

APScheduler also exposes options that matter in production:

scheduler.add_job( collect_metrics, "interval", minutes=1, max_instances=1, coalesce=True, misfire_grace_time=60, )

max_instances=1 prevents the same job from running concurrently if a previous run is still executing. coalesce=True merges missed runs into a single execution when the scheduler was down or busy. misfire_grace_time defines how long after the scheduled time a missed run can still execute before it is discarded.

For persistence, APScheduler supports job stores backed by SQLAlchemy, MongoDB, or Redis. A persisted job store means the schedule survives a process restart, which the schedule library cannot do.

Handling Overlap, Missed Runs, and Drift

The three operational problems that distinguish scheduling approaches are overlap, missed runs, and drift.

Overlap occurs when a job takes longer than its interval. With time.sleep(), the next run starts immediately after the previous one finishes, so long jobs push the schedule later. With the schedule library, jobs run in the calling thread, so a long job blocks all other registered jobs. With APScheduler, the default thread pool can start a new instance of the same job while the previous one is still running; max_instances=1 is the correct guard.

Missed runs happen when the process is down at the scheduled time. Neither time.sleep() nor schedule remembers what was missed. APScheduler's coalesce and misfire_grace_time provide a bounded recovery window: if the scheduler comes back within the grace period, the missed job runs once.

Drift is the cumulative timing error introduced by time.sleep(). The schedule library reduces drift because run_pending() executes jobs based on their registered due time rather than a fixed sleep duration, but the loop's own time.sleep(1) still adds up to a second of latency per run. APScheduler's interval trigger schedules the next run relative to the previous start time, which keeps intervals consistent even when a job runs long.

Choosing the Right Scheduler for Your Use Case

Criteriontime.sleep()scheduleAPScheduler
Setup costNonepip install schedulepip install apscheduler
Cron-like timesNoDay and time onlyFull cron trigger
Concurrency controlNoneNonemax_instances, thread pool
Missed-run recoveryNoNocoalesce, misfire_grace_time
Job persistenceNoNoSQLAlchemy, MongoDB, Redis
Best fitOne-off scriptsSmall internal toolsLong-running services

Use time.sleep() when the script runs once, the interval precision does not matter, and you do not need recovery behavior. Use schedule when you want readable interval declarations and the job set is small and static. Use APScheduler when jobs must survive restarts, run concurrently, follow cron rules, or be monitored in production.

Running Scheduled Jobs in Production

A scheduler inside a Python process only works while that process is alive. For production, the scheduler should run as a dedicated service with a restart policy, whether that is a systemd unit or a Docker container with restart: unless-stopped.

When embedding a scheduler in a web application, use BackgroundScheduler so the request loop is not blocked:

from apscheduler.schedulers.background import BackgroundScheduler scheduler = BackgroundScheduler() scheduler.add_job(collect_metrics, "interval", minutes=1) scheduler.start()

The scheduler runs in a background thread, and you should call scheduler.shutdown() during application shutdown to stop the thread cleanly.

For jobs that must run even when the Python process is down, system cron is a legitimate alternative. A cron entry invokes a Python script directly:

* * * * * /usr/bin/python3 /opt/app/collect_metrics.py
0 * * * * /usr/bin/python3 /opt/app/collect_hourly.py
30 9 * * * /usr/bin/python3 /opt/app/collect_daily.py

Each line runs the script at the specified interval, and cron handles the process lifecycle. The tradeoff is that cron gives you no in-process state, no Python-level error handling around the schedule, and no way to coordinate with other parts of the application. For a single server with static schedules, cron is often simpler than running a scheduler daemon. For distributed or stateful scheduling, APScheduler with a persisted job store is the stronger choice.

python schedule periodic jobs every minute hour and day: Pra | RYUSLOG DEV