async-watchdog
A fast, simple asyncio watchdog for monitoring heartbeats in async Python
applications. If a monitored process stops reporting activity within the
configured timeout, async-watchdog fires a callback or logs a warning —
keeping your system observable without adding runtime dependencies.
Installation
pip install async-watchdog
Quick Start
import asyncio
from async_watchdog import Watchdog
async def main():
wd = Watchdog(timeout=5.0)
await wd.start()
# Signal that the process is alive
wd.beat()
# ... do work, call wd.beat() periodically ...
await wd.stop()
asyncio.run(main())
The watchdog waits for the first beat() before starting the timeout
clock, so slow-starting processes are not penalised.
Usage
Async callback on timeout
The recommended pattern for I/O-bound reactions (sending alerts, closing connections, restarting tasks):
import asyncio
from async_watchdog import Watchdog
async def on_timeout():
print("No heartbeat received — restarting worker...")
# await send_alert(...) # safe: async I/O is fine here
async def worker(wd: Watchdog):
for _ in range(5):
await asyncio.sleep(1)
wd.beat()
# Worker stops sending beats — timeout will fire after this
async def main():
wd = Watchdog(timeout=3.0, on_timeout=on_timeout)
await wd.start()
try:
await worker(wd)
await asyncio.sleep(5) # Wait for timeout to trigger
finally:
await wd.stop()
asyncio.run(main())
Sync callback on timeout
Synchronous callbacks work too. Use them for lightweight, non-blocking reactions such as setting a flag or incrementing a counter:
import asyncio
from async_watchdog import Watchdog
timeout_count = 0
def on_timeout():
global timeout_count
timeout_count += 1
print(f"Timeout #{timeout_count} detected")
async def main():
wd = Watchdog(timeout=2.0, on_timeout=on_timeout)
await wd.start()
try:
wd.beat() # First beat starts the clock
await asyncio.sleep(6) # No more beats — callback fires ~every 2 s
finally:
await wd.stop()
asyncio.run(main())
Warning: Sync callbacks must not perform blocking operations (see Security).
No callback — logging only
Omit on_timeout to get a WARNING log entry on every missed heartbeat
window. Useful during development or when you only need observability:
import asyncio
from async_watchdog import Watchdog
async def main():
wd = Watchdog(timeout=5.0) # No on_timeout
await wd.start()
try:
wd.beat()
await asyncio.sleep(12) # Watchdog logs warnings at t=5, t=10
finally:
await wd.stop()
asyncio.run(main())
Output:
[Watchdog] WARNING Timeout of 5.0 seconds reached without heartbeat.
[Watchdog] WARNING Timeout of 5.0 seconds reached without heartbeat.
Integration in a real worker loop
A typical pattern for a long-running background worker that must report liveness to the watchdog on every iteration:
import asyncio
from async_watchdog import Watchdog
async def on_worker_timeout():
print("Worker is unresponsive — triggering recovery")
# await notify_ops_team(...)
async def process_item(item):
await asyncio.sleep(0.1) # Simulate work
return item * 2
async def worker(wd: Watchdog, queue: asyncio.Queue):
while True:
item = await queue.get()
await process_item(item)
wd.beat() # Report liveness after each successful iteration
queue.task_done()
async def main():
queue = asyncio.Queue()
wd = Watchdog(timeout=10.0, on_timeout=on_worker_timeout)
await wd.start()
worker_task = asyncio.create_task(worker(wd, queue))
try:
for i in range(20):
await queue.put(i)
await asyncio.sleep(0.5)
await queue.join()
finally:
worker_task.cancel()
await wd.stop()
asyncio.run(main())
API Reference
Watchdog(timeout, on_timeout=None, logger=None)
Creates a new watchdog instance.
| Parameter | Type | Required | Description |
|---|---|---|---|
timeout |
float |
Yes | Seconds to wait for a heartbeat before triggering. Must be in [0.001, 86400]. inf, nan, zero, and negatives are rejected. |
on_timeout |
callable | None |
No | Callback invoked on each missed heartbeat window. Accepts both sync and async callables. If None, a WARNING is logged instead. |
logger |
logging.Logger | None |
No | Custom logger instance. Defaults to the package logger with [Watchdog] prefix. |
Raises:
TypeError— iftimeoutis not a numericintorfloat(booleans excluded), or ifon_timeoutis not callable.ValueError— iftimeoutis outside[0.001, 86400]or is not finite.
Methods
await wd.start()
Starts the watchdog background task. The watchdog blocks until the first
beat() is received before beginning timeout monitoring.
- Idempotent: safe to call multiple times; only one background task is ever created.
- Concurrency-safe: protected by an
asyncio.Lock.
wd.beat()
Signals that the monitored process is alive. Resets the timeout window.
Synchronous — call it from anywhere in your async code without await.
await wd.stop()
Cancels the background task and cleans up. Safe to call even if start()
was never called. After stop(), the watchdog can be restarted with
start().
Callback behaviour
| Scenario | Behaviour |
|---|---|
| Async callback | Awaited directly; non-blocking for the event loop |
| Sync callback | Called inline; must not block (see Security) |
| Callback raises an exception | Exception type logged at ERROR; watchdog continues running |
Callback exceeds timeout |
Cancelled and logged at ERROR; watchdog continues running |
| No callback provided | WARNING logged on every missed heartbeat window |
Security
Blocking sync callbacks
Synchronous on_timeout callbacks run directly on the asyncio event loop.
Any blocking operation inside a sync callback will freeze the entire event
loop for the duration of the call, preventing all other coroutines from
making progress.
Do not use in sync callbacks:
time.sleep()- Blocking file or network I/O
- CPU-intensive computation
Safe alternatives:
# Option 1: Use an async callback for I/O
async def on_timeout():
await asyncio.sleep(1) # Non-blocking
await send_alert_via_http() # Non-blocking
# Option 2: Offload blocking work to a thread pool
import asyncio
def blocking_work():
time.sleep(1) # Runs in a thread, not the event loop
async def on_timeout():
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, blocking_work)
Log injection
All exception messages written to logs are sanitised (control characters stripped) to prevent log injection attacks.
Requirements
- Python:
>= 3.10 - Runtime dependencies: none
- Optional:
uvloopfor higher throughput (drop-in replacement for the default asyncio event loop)
License
MIT — Álvaro Suárez Lozano
Metadata
Release files for async-watchdog 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| async_watchdog-0.0.1.tar.gz | 10.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| async_watchdog-0.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 17.3 kB
Release files / async_watchdog-0.0.1.tar.gz
| Download URL | async_watchdog-0.0.1.tar.gz |
|---|---|
| Size | 10.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7136da4354cbec543953ccf440b3f10516864036af3477ecb08075662ccc4f4a
|
|
BLAKE2b-256 checksum How to use checksums |
cd0d223dfaf870bacec8a65354d6897ac949eb31cde060962fb949e24c7b816c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.13
|
Release files / async_watchdog-0.0.1-py3-none-any.whl
| Download URL | async_watchdog-0.0.1-py3-none-any.whl |
|---|---|
| Size | 7.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
312d457aa4f156d24ea5f116f453cc39b71fdcc73e8368bbda25ae65d57deecf
|
|
BLAKE2b-256 checksum How to use checksums |
234108ea05a0742b032e0c182e0f05281baf5117afd5c689f06802f393228843
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.13
|
Security for async-watchdog 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.