The sched module implements a simple in-memory event scheduler. It maintains a queue ordered by time and priority, waits until events are due, and executes callbacks. It is useful in scripts, simulations, tests, small services, and applications that need local timing without an external dependency.
sched is not cron, a distributed queue, or durable job infrastructure. Events disappear when the process exits, long callbacks delay later work, and the module does not provide retries, persistence, or multi-machine execution. Use it for controlled local scheduling.
Create a scheduler
The constructor accepts a time function and a delay function. Modern defaults are based on time.monotonic and time.sleep.
import sched
import time
scheduler = sched.scheduler(time.monotonic, time.sleep)
A monotonic clock is appropriate for intervals because it does not move backward when wall-clock time changes.
Schedule with a relative delay
enter() schedules an action after a delay.
def run_task(name):
print("running", name)
scheduler.enter(5, 1, run_task, argument=("task",))
scheduler.run()
The second argument is priority. Lower numbers run first when events share the same timestamp.
Arguments and keyword arguments
Use argument for positional parameters and kwargs for named parameters.
scheduler.enter(
2,
1,
send,
argument=(destination,),
kwargs={"attempt": 1},
)
Explicit arguments are easier to test and log than closures that capture mutable state.
Schedule at an absolute time
enterabs() receives a value compatible with the scheduler’s time function.
when = time.monotonic() + 10
scheduler.enterabs(when, 1, run_task, argument=("absolute",))
Do not mix time.time() timestamps with a scheduler based on time.monotonic().
Priorities
When events have the same due time, priority controls their order.
scheduler.enter(1, 10, run_task, argument=("normal",))
scheduler.enter(1, 1, run_task, argument=("urgent",))
Priority does not preempt a callback that is already running.
The Event object
enter() and enterabs() return an event object that can later be cancelled.
event = scheduler.enter(30, 1, run_task)
Store the reference alongside the application’s logical task identifier.
Cancel an event
cancel() removes a pending event.
try:
scheduler.cancel(event)
except ValueError:
print("the event is no longer pending")
Cancellation fails when the event has already executed, was removed, or does not belong to the queue.
Inspect the queue
The queue property exposes pending events in execution order.
for event in scheduler.queue:
print(event.time, event.priority, event.action)
Use it for observability, not for direct mutation.
Run without blocking for the next event
run(blocking=False) executes due events and returns information about the next deadline when one exists.
next_deadline = scheduler.run(blocking=False)
This mode helps integrate the scheduler into a GUI, main loop, or service that performs other work.
Long callbacks
The scheduler runs callbacks sequentially. A ten-second action delays every event that becomes due during those ten seconds.
Keep callbacks short or submit work to a bounded executor. Do not create an unlimited thread per event.
Late events
When the process is busy, sched does not automatically drop overdue events. It executes them as soon as possible in queue order.
The application must decide whether stale work still matters. Compare planned and actual time and define a tolerance.
Recurring tasks
A callback can schedule its next occurrence.
INTERVAL = 60
def recurring(next_time):
perform_work()
following = next_time + INTERVAL
scheduler.enterabs(following, 1, recurring, argument=(following,))
first = time.monotonic() + INTERVAL
scheduler.enterabs(first, 1, recurring, argument=(first,))
Calculating from the planned time reduces drift.
Avoid drift
If a callback calls enter(INTERVAL, ...) only after finishing, task duration is added to every cycle.
Use absolute planned times for a fixed cadence. Use relative delays only when the rule is “wait N seconds after completion.”
Callback failures
An exception escapes from run(). The scheduler remains structurally consistent, but later events wait until the caller invokes run() again.
def safe_action():
try:
perform_work()
except Exception:
logger.exception("scheduled action failed")
Catch errors only when there is a clear policy. Critical failures may need to terminate the process.
Retries
sched does not retry automatically.
Retry only transient failures, with limits, backoff, jitter, and idempotency. Do not retry validation errors forever.
Threads
Modern implementations allow scheduling from multiple threads, but the application still needs clear lifecycle, cancellation, and callback rules.
Do not hold application locks while callbacks execute if those callbacks may acquire the same resources.
Earlier events inserted by another thread
If one thread is waiting for a distant event and another inserts an earlier one, the outer integration must ensure the loop rechecks the queue.
For complex concurrent systems, a condition-based loop, asyncio, or a dedicated scheduler may be a better fit.
Integrate with ThreadPoolExecutor
The scheduler can submit work to a bounded pool.
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
scheduler.enter(1, 1, executor.submit, argument=(perform_work,))
Observe returned futures and shut the executor down cleanly. See Python concurrent.futures.
Shutdown
Maintain a stop flag or event. Stop creating new recurrences, cancel pending events when appropriate, and wait for submitted work.
An in-memory scheduler cannot guarantee pending tasks survive shutdown.
Wall-clock schedules
For rules such as “09:00 local time,” convert civil time carefully or use infrastructure that handles time zones, daylight-saving transitions, and persistence.
time.monotonic() measures intervals; it is not a calendar timestamp.
Deterministic tests
Inject fake time and delay functions.
class Clock:
def __init__(self):
self.now = 0
def time(self):
return self.now
def sleep(self, delay):
self.now += delay
clock = Clock()
test_scheduler = sched.scheduler(clock.time, clock.sleep)
Tests can then advance instantly without real sleeps.
Persistence
If events must survive restarts, store job intent in a database or durable queue and rebuild local scheduling at startup.
Do not serialize arbitrary callbacks. Persist an approved task type and validated parameters.
Security
Never let users schedule arbitrary Python callables. Map approved commands to known functions.
Apply limits to task count, frequency, priority, and argument size.
Observability
Record logical task ID, planned time, actual start, delay, duration, result, and attempt.
Queue length, lateness, and failure metrics reveal blocked callbacks and capacity problems.
When to choose another tool
Use cron or Task Scheduler for independent processes, asyncio for asynchronous applications, durable queues for persistent jobs, and richer scheduling libraries for calendar rules.
sched is best for a small local time-ordered queue.
Common mistakes
Common failures include mixing clocks, running long callbacks, introducing recurring drift, assuming persistence, ignoring exceptions, forgetting cancellation, treating priority as preemption, and using real sleeps in tests.
Conclusion
sched provides local event scheduling through a time-ordered queue. Use a monotonic clock for intervals, absolute deadlines for fixed cadence, priorities for tie-breaking, and short callbacks.
Add cancellation, observability, and explicit error policy. Consult the official sched documentation and Python concurrent.futures.







