Vivere

Vivere / Answers

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