Fast and simple async watchdog to monitor heartbeats in asyncio applications.
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file async_watchdog-0.0.1.tar.gz.
File metadata
- Download URL: async_watchdog-0.0.1.tar.gz
- Upload date:
- Size: 10.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7136da4354cbec543953ccf440b3f10516864036af3477ecb08075662ccc4f4a
|
|
| MD5 |
ca0ad81046a029c59b69acf468537404
|
|
| BLAKE2b-256 |
cd0d223dfaf870bacec8a65354d6897ac949eb31cde060962fb949e24c7b816c
|
File details
Details for the file async_watchdog-0.0.1-py3-none-any.whl.
File metadata
- Download URL: async_watchdog-0.0.1-py3-none-any.whl
- Upload date:
- Size: 7.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
312d457aa4f156d24ea5f116f453cc39b71fdcc73e8368bbda25ae65d57deecf
|
|
| MD5 |
53bfc4281d427f339bf9d46bac3df866
|
|
| BLAKE2b-256 |
234108ea05a0742b032e0c182e0f05281baf5117afd5c689f06802f393228843
|