How to monitor a Python script that runs on a schedule
Four lines of Python turn a scheduled script into one that tells you when it stops, fails, or takes twice as long as usual.
A scheduled Python script has three ways to disappoint you: it can fail, it can hang, and it can never start. Logging covers the first, sometimes the second, and never the third. Here is a small amount of code that covers all three.
The minimum: report success
Call a heartbeat URL when the work has finished, not when it starts. The position matters — a ping at the top of the file proves only that Python launched.
import requests
PING = "https://vivere.dev/p/<your-monitor-id>"
def main():
export_yesterdays_orders()
if __name__ == "__main__":
main()
requests.get(PING, timeout=10)
Always pass a timeout. Without one, requests waits indefinitely, and a monitoring call that hangs is worse than no monitoring call at all.
Report failures too, with the traceback
Waiting for a missed deadline tells you something is wrong up to a whole period late. Reporting the failure directly tells you at once, and carrying the last part of the traceback means the alert contains the answer:
import sys, traceback, requests
PING = "https://vivere.dev/p/<your-monitor-id>"
def main():
export_yesterdays_orders()
if __name__ == "__main__":
try:
main()
except Exception:
requests.post(PING + "/fail", data=traceback.format_exc()[-10000:], timeout=10)
raise
requests.get(PING, timeout=10)
The raise matters: the script should still exit non-zero so that whatever ran it also knows.
As a decorator, for scripts you do not want to restructure
import functools, time, traceback, requests
def monitored(ping_url, timeout=10):
"""Report start, success and failure of the wrapped function."""
def decorate(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
requests.get(ping_url + "/start", timeout=timeout)
started = time.monotonic()
try:
result = fn(*args, **kwargs)
except BaseException:
requests.post(ping_url + "/fail",
data=traceback.format_exc()[-10000:], timeout=timeout)
raise
requests.post(ping_url,
data=f"ok in {time.monotonic() - started:.1f}s", timeout=timeout)
return result
return wrapper
return decorate
@monitored("https://vivere.dev/p/<your-monitor-id>")
def main():
export_yesterdays_orders()
Pinging /start as well as the end is what gives you durations. Once a monitor knows how long the job usually takes, a run that takes three times as long is visible before it becomes a failure. Catching BaseException rather than Exception means a KeyboardInterrupt or a SystemExit is reported too, which covers the job killed by a deployment or by the OOM killer.
Without adding a dependency
If the script must stay dependency-free, the standard library is enough:
import urllib.request
def ping(url, body=None):
try:
urllib.request.urlopen(url, data=body, timeout=10).read()
except Exception:
pass # never let monitoring break the job
That except is deliberate. A monitoring call should never be the reason a working job fails; a missed ping produces an alert, which is the correct outcome anyway.
Or wrap the command instead of the code
If you would rather not touch the script at all, wrap it. Vivere's CLI reports the start, the exit code, the duration and the tail of everything the command printed, including when it is killed:
vivere run --url https://vivere.dev/p/<id> -- python /srv/app/export.py
The part logging cannot do
Every technique above still assumes Python ran. The failure that costs the most is the one where it did not: a disabled cron entry, a machine that was off, a container that never started, a virtualenv deleted by a deployment. Nothing inside the script can report those, because nothing inside the script executed.
That is what the deadline is for. Tell the monitor the script runs every day at 02:00 with a 30-minute grace, and the absence of the ping is itself the alert — the one signal that covers every way a scheduled script can fail to happen.
Common questions
How do I know if my scheduled Python script stopped running?
Have the script call an external URL when it finishes successfully, and have something outside the machine alert you when that call does not arrive on schedule. Logging inside the script cannot tell you about the run that never started, because nothing ran to write the log.
How do I get alerted when a Python script raises an exception?
Wrap the entry point in try/except, report the failure to an external endpoint in the except block, and re-raise. Sending the last part of the traceback with the failure means the alert tells you what broke rather than only that something did.
Find out without looking
Vivere watches for the ping that does not arrive. Add one line to the job, pick where alerts should land, and you hear about the run that never happened.
Start free Read the quickstart
Ten monitors, a status page, email and webhook alerts. No card.
Last reviewed September 2026.
Related
- Cron job not running: how to find out why — A cron job stopped running and nothing told you. The five usual causes in the order worth checking, how to read the logs, and how to find out next time without looking.
- A dead man's switch for your scripts — What a dead man's switch is, why it catches failures that error-based alerting cannot, how to build one with cron and curl, and the timing rules that stop it crying wolf.
- How to know if your backup actually ran — Backups fail quietly. How to verify a backup ran, why exit code zero is not proof, what to check about size and restore, and how to be told the night it does not happen.