Monitoring Litestar Python Applications with Vigilmon
Litestar (formerly Starlite) is a fast, async Python framework that takes first-class typing, dependency injection, and OpenAPI generation seriously. It's a compelling alternative to FastAPI for teams that want more structure out of the box.
But like any web framework, Litestar doesn't tell you when your app goes down. That's an external concern, and this guide covers exactly how to handle it: adding structured health endpoints to your Litestar app, then hooking them into Vigilmon for multi-region uptime monitoring and alerts.
What's different about Litestar monitoring
If you've already read monitoring guides for FastAPI, Django, or Flask, the concepts here are the same. The implementation is different because Litestar has:
- Typed route handlers via
@get,@post, etc. decorators with return type annotations - Dependency injection built into the framework — use it to inject your DB connection into health checks
- Built-in OpenAPI — your health endpoint will automatically appear in your API docs
DTOFactory/ dataclasses — returning structured responses is idiomatic
The patterns below fit the Litestar way of doing things.
Step 1: Add a structured health check endpoint
Litestar's route handlers are functions with type annotations. Create a dedicated health router:
# app/health.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from litestar import Router, get
from litestar.status_codes import HTTP_200_OK, HTTP_503_SERVICE_UNAVAILABLE
from litestar.response import Response
Define a response dataclass:
@dataclass
class ServiceCheck:
status: Literal["ok", "error"]
detail: str | None = None
@dataclass
class HealthResponse:
status: Literal["ok", "degraded"]
checks: dict[str, ServiceCheck]
Add the health route:
async def check_database(db_url: str) -> ServiceCheck:
"""Verify database connectivity."""
try:
import asyncpg
conn = await asyncpg.connect(db_url, timeout=3)
await conn.execute("SELECT 1")
await conn.close()
return ServiceCheck(status="ok")
except Exception as exc:
return ServiceCheck(status="error", detail=str(exc))
async def check_redis(redis_url: str | None) -> ServiceCheck | None:
if not redis_url:
return None
try:
import redis.asyncio as aioredis
r = aioredis.from_url(redis_url)
await r.ping()
await r.aclose()
return ServiceCheck(status="ok")
except Exception as exc:
return ServiceCheck(status="error", detail=str(exc))
@get("/health", status_code=HTTP_200_OK, tags=["ops"])
async def health_handler(
db_url: str, # injected via Litestar DI
redis_url: str | None = None,
) -> Response[HealthResponse]:
db = await check_database(db_url)
checks: dict[str, ServiceCheck] = {"database": db}
redis = await check_redis(redis_url)
if redis is not None:
checks["redis"] = redis
all_ok = all(c.status == "ok" for c in checks.values())
return Response(
content=HealthResponse(
status="ok" if all_ok else "degraded",
checks=checks,
),
status_code=HTTP_200_OK if all_ok else HTTP_503_SERVICE_UNAVAILABLE,
)
Wire up the router and inject the dependencies in your app:
# app/main.py
import os
from litestar import Litestar
from litestar.di import Provide
from app.health import health_handler
def get_db_url() -> str:
return os.environ["DATABASE_URL"]
def get_redis_url() -> str | None:
return os.environ.get("REDIS_URL")
app = Litestar(
route_handlers=[health_handler],
dependencies={
"db_url": Provide(get_db_url),
"redis_url": Provide(get_redis_url),
},
)
Test it:
uvicorn app.main:app --reload
curl http://localhost:8000/health
Healthy response:
{
"status": "ok",
"checks": {
"database": {"status": "ok", "detail": null}
}
}
Degraded response (HTTP 503):
{
"status": "degraded",
"checks": {
"database": {
"status": "error",
"detail": "could not connect to server: Connection refused"
}
}
}
The 503 is the signal your external monitor needs to open an incident automatically.
Step 2: Add liveness vs readiness separation
For containerised deployments, distinguish between the two:
# app/health.py — add these two handlers
@get("/health/live", tags=["ops"], status_code=HTTP_200_OK)
async def liveness_handler() -> dict[str, str]:
"""Is the process alive? For container liveness probes."""
return {"status": "ok"}
@get("/health/ready", tags=["ops"])
async def readiness_handler(db_url: str) -> Response[dict]:
"""Are dependencies ready? For container readiness probes."""
db = await check_database(db_url)
if db.status != "ok":
return Response(
content={"status": "not_ready", "database": db.detail},
status_code=HTTP_503_SERVICE_UNAVAILABLE,
)
return Response(
content={"status": "ready"},
status_code=HTTP_200_OK,
)
- Vigilmon monitors
/health/ready— this reflects whether your app is actually serving traffic correctly - Kubernetes uses
/health/livefor the liveness probe — it won't restart a healthy pod just because Postgres is temporarily slow
Step 3: Set up external monitoring with Vigilmon
With your health endpoints live, point Vigilmon at them:
- Sign up at vigilmon.online — free tier, no credit card required
- New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval to 5 minutes
- Save
Vigilmon probes from multiple geographic regions. If any region gets a non-2xx response or timeout, it opens an incident and sends an alert.
Add a keyword monitor
For deeper assurance, add a keyword monitor that asserts the response body contains "status":"ok":
- New Monitor → Keyword
- URL:
https://yourdomain.com/health - Keyword:
"status":"ok"
This catches the case where your app returns HTTP 200 but serves a degraded response — which you'd otherwise miss with a status-only check.
Recommended Litestar monitor set
| Monitor | URL | Type | What it catches |
|---|---|---|---|
| App health | /health | HTTP | Database down, Redis down |
| Keyword check | /health | Keyword | Degraded response despite 200 |
| Readiness | /health/ready | HTTP | App in unready state |
| Root route | / | HTTP | App startup failure |
Step 4: Heartbeat monitoring for background tasks
If your Litestar app has background workers or scheduled tasks, HTTP endpoint checks won't catch silent worker failures.
Using APScheduler with Litestar's lifespan
# app/main.py
import os
import httpx
import logging
from contextlib import asynccontextmanager
from collections.abc import AsyncGenerator
from litestar import Litestar
from apscheduler.schedulers.asyncio import AsyncIOScheduler
logger = logging.getLogger(__name__)
scheduler = AsyncIOScheduler()
async def nightly_cleanup() -> None:
"""Run cleanup and ping heartbeat on success."""
try:
# Your actual logic
await do_cleanup()
heartbeat_url = os.environ.get("CLEANUP_HEARTBEAT_URL")
if heartbeat_url:
async with httpx.AsyncClient() as client:
await client.get(heartbeat_url, timeout=10)
logger.info("nightly_cleanup: heartbeat pinged")
except Exception:
logger.exception("nightly_cleanup: failed, skipping heartbeat")
@asynccontextmanager
async def lifespan(_: Litestar) -> AsyncGenerator[None, None]:
scheduler.add_job(
nightly_cleanup,
trigger="cron",
hour=2,
minute=0,
id="nightly_cleanup",
replace_existing=True,
)
scheduler.start()
yield
scheduler.shutdown()
app = Litestar(
route_handlers=[health_handler, liveness_handler, readiness_handler],
lifespan=[lifespan],
)
# .env
CLEANUP_HEARTBEAT_URL=https://vigilmon.online/api/heartbeat/your-unique-token
In Vigilmon:
- New Monitor → Heartbeat
- Expected interval: 25 hours (buffer for a nightly job)
- Copy the ping URL and add to your environment
Step 5: Configure alerts
Slack:
- Create an incoming webhook in your Slack workspace
- Vigilmon: Notifications → New Channel → Slack
- Paste the URL and enable on your monitors
Discord:
- In your Discord server: Integrations → Webhooks → New Webhook
- Vigilmon: Notifications → New Channel → Discord
- Paste and enable
When your app goes down:
🔴 DOWN: yourdomain.com/health
Status: 503 Service Unavailable
Regions: US-East, EU-West, AP-Southeast
Started: 3 minutes ago
When a scheduled task misses its heartbeat:
🔴 MISSED: nightly_cleanup heartbeat
Expected every: 25 hours
Last ping: 27 hours ago
Step 6: Docker and Gunicorn setup
For production Litestar deployments:
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=30s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/live')"
CMD ["gunicorn", "app.main:app", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--workers", "4", \
"--bind", "0.0.0.0:8000"]
The HEALTHCHECK directive gives Docker visibility into app health for automatic restarts. Vigilmon provides the external alert when the container isn't even reachable.
Step 7: Public status page
In Vigilmon:
- Status Pages → New Status Page
- Name it and add your monitors
- Share the URL with your users
Add it to your Litestar app's OpenAPI metadata so it appears in the docs:
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.spec import Contact
app = Litestar(
openapi_config=OpenAPIConfig(
title="My API",
version="1.0.0",
description="Status page: https://status.yourdomain.com",
contact=Contact(name="Ops Team", email="ops@yourdomain.com"),
),
route_handlers=[health_handler, liveness_handler, readiness_handler],
)
What you've built
| What | How |
|---|---|
| Typed health endpoint | /health with dependency-injected DB check, returns 503 on failure |
| Liveness vs readiness | /health/live and /health/ready — separate concerns |
| External uptime monitoring | Vigilmon HTTP monitor (multi-region) |
| Keyword assertion | Verify "status":"ok" in response body |
| Background task monitoring | APScheduler + heartbeat ping on success |
| Instant alerts | Slack/Discord notifications on down + recovery |
| Container health | Docker HEALTHCHECK directive |
| Public status page | Vigilmon status page linked from API docs |
Litestar's typed handler system makes health endpoints clean to write and self-documenting in OpenAPI. Pair that with Vigilmon's external monitoring and you have full visibility into your application's health without adding any heavy dependencies.
Get started free at vigilmon.online — your first monitor is running in under a minute.