Skip to content

Deployment and runtime behavior

Run Watchpost as a long-lived ASGI application and have Checkmk poll it over HTTP. The Checkmk integration guide covers collection and service discovery. The application requires Python 3.13 or newer.

Start and validate an application

From the application project, install the CLI extra and an ASGI server, then validate the configuration before starting the server:

uv add 'watchpost[cli]' uvicorn
uv run watchpost --app my_watchpost:app verify-check-configuration
uv run watchpost --app my_watchpost:app run-checks
uv run uvicorn my_watchpost:app --host 0.0.0.0 --port 8000 --workers 1

Verification returns a nonzero exit status for scheduling or hostname errors. run-checks waits for checks and disables result caching by default; use --cache to exercise caching. HTTP uses the application's background executor. Configure checks, datasources, and strategies before serving requests; validation and resolved configuration are cached for the lifetime of the application.

Use one ASGI worker as a starting point. Every process owns its own datasource instances, executor, event loop, and pending-work registry. Multiple workers or replicas can execute the same check simultaneously. A shared disk or Redis cache shares results, but does not provide a distributed scheduling lock. Partition checks or arrange collection so each check has one responsible application instance when duplicate execution is undesirable.

The HTTP endpoints have no built-in authentication. Restrict access to the Checkmk site through network policy or an authenticated reverse proxy. Use HTTPS when collection crosses an untrusted network.

Polling, scheduling, and caching

A request to / drives execution and streams Checkmk agent output. Watchpost has no independent periodic scheduler: cache_for="5m" makes results reusable for five minutes; it does not cause a check to run every five minutes without polls. Configure the collection interval in Checkmk to suit the desired freshness.

For each check and its declared target environment:

Situation Poll behavior
SCHEDULE with a fresh cached result Return the cached result.
SCHEDULE without a reusable result Submit work if none is pending, then pick up completed results.
Work is still running Return an available prior result, otherwise UNKNOWN until a later poll retrieves completion.
SKIP Reuse an available prior result; otherwise return UNKNOWN.
DONT_SCHEDULE Emit no result for that pair from this instance.

The first HTTP poll commonly returns UNKNOWN because execution has started but has not finished. A later poll collects the result. With a positive cache_for, an expired cache entry may be returned once while refreshing; stale cache entries are not retained indefinitely.

With cache_for=None (or zero), the latest result is retained in application memory while a new execution runs. This fallback does not delay scheduling, is not written to the configured cache storage, and is replaced by the next completed result, including failures. A prior cache entry from an earlier configuration can seed the fallback, but cannot delay pickup of a new result. Newly collected uncached results do not survive application restarts. run_check(..., use_cache=False) bypasses both the cache and this fallback. If a check never finishes, the previous result remains visible; configure timeouts on external I/O.

Both cached and uncached checks have at most one outstanding execution per check/environment pair through the application API. Once a completed uncached result has been picked up, the next poll may start another execution. max_workers limits synchronous worker threads; async checks run as tasks on the executor's event loop. For a fixed application, pending work is bounded by the number of check/environment pairs. Direct executor callers that explicitly use resubmit=True opt into overlapping work.

The default cache is in memory and disappears at process exit. Set Watchpost(check_cache_storage=...) to a DiskStorage or RedisStorage instance for persistence. Use a separate disk directory or Redis namespace for unrelated applications. Disk entries use pickle and the cache directory must be writable only by trusted processes. Disk writes replace complete files atomically; missing or corrupt entries are treated as misses. Short filesystem locks coordinate publication and expiry across threads and processes, preventing expiry from deleting a concurrent replacement. Use a filesystem that supports advisory file locking; persistent lock files and shard directories are retained. Persistent storage does not restore pending jobs after a restart.

Generated checks are identified by their qualified function name and service name. Supply distinct @check(id="...") values when those names would collide for the same target. Explicit IDs also provide stable keys when code is moved. Changing an identity causes a cache miss. The current versioned keys do not reuse older name-only cache entries.

Cache.memoize() supports synchronous and async def functions. Async functions cache their awaited values, and formatted keys include omitted argument defaults. Exceptions and cancellation are not cached. Storage operations remain synchronous, and simultaneous misses may each execute the memoized function.

Timeouts and shutdown

Set timeouts on external I/O in every check. Use synchronous client calls inside def checks, and await async clients inside async def checks. Blocking I/O in an async check blocks the event loop shared by all async checks. Client connect/read timeouts bound those operations; use asyncio.timeout() when an async check also needs a deadline for the whole operation.

Watchpost does not impose a check deadline or forcibly stop running Python threads. A blocked check retains its pending slot instead of creating another job on every poll. Ensure synchronous operations finish within a time compatible with the ASGI server's shutdown grace period.

ASGI lifespan validates configuration on startup and closes Watchpost's own executor on shutdown, including after failed startup. Shutdown cancels async checks and queued thread jobs, waits for running synchronous checks and async cleanup, stops the event loop, and releases executor resources. Use context managers inside checks to close HTTP clients and other per-call resources. Datasources have no automatic lifecycle/close hook; application-owned long-lived resources need their own lifecycle management.

For standalone use, call app.shutdown() in finally. An executor passed via Watchpost(executor=...) or assigned to app.executor belongs to the caller and must be shut down explicitly. If you replace an application-created executor, app.shutdown() still closes that original executor. Direct executor users can drain work with shutdown(wait=True) or request cancellation with shutdown(wait=True, cancel_futures=True).

Diagnostics and service identity

Endpoint or service Meaning
/healthcheck HTTP 204 means the app can respond; it does not prove checks or their dependencies are healthy.
/executor/statistics Snapshot of executor counters and pending work in this process.
/executor/errored Executor error diagnostics; inspect check output for failures handled as monitoring results.
Watchpost: executed checks Number of check/environment pairs whose actual decision was SCHEDULE in this poll. Includes cache hits and already-running work; excludes skipped pairs.

The synthetic service name is retained for existing Checkmk installations. Its count describes eligibility, not completed invocations or the number of emitted services. Its details list each eligible check and actual target environment.

Datasource construction failures affect their check's output. An unavailable datasource produces UNKNOWN or an available prior result with failure details; other exceptions produce CRIT. Error handlers can expand these into the same hosts and service suffixes normally emitted by the check. Failed factory probes are retried on later polls; execution uses the recovered datasource’s scheduling restrictions before running the check. Shared datasource construction is serialized per instance, while unrelated datasources can initialize concurrently.

Each (hostname, service_name) must be unique across collected results. Duplicate identities become one UNKNOWN service with conflict details in Checkmk. Give checks distinct service names, result suffixes, or hosts. get-check-hostnames shows configuration-derived names; it cannot predict per-result dynamic hosts or service suffixes. Ensure those hosts exist in Checkmk and run discovery after collection.