tutorial

Monitoring Litestar Python Applications with Vigilmon

Litestar is a modern async Python framework with first-class typing and a batteries-included feel. Learn how to add structured health routes and set up external uptime monitoring with Vigilmon.

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/live for 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:

  1. Sign up at vigilmon.online — free tier, no credit card required
  2. New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval to 5 minutes
  5. 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":

  1. New Monitor → Keyword
  2. URL: https://yourdomain.com/health
  3. 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:

  1. New Monitor → Heartbeat
  2. Expected interval: 25 hours (buffer for a nightly job)
  3. Copy the ping URL and add to your environment

Step 5: Configure alerts

Slack:

  1. Create an incoming webhook in your Slack workspace
  2. Vigilmon: Notifications → New Channel → Slack
  3. Paste the URL and enable on your monitors

Discord:

  1. In your Discord server: Integrations → Webhooks → New Webhook
  2. Vigilmon: Notifications → New Channel → Discord
  3. 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:

  1. Status Pages → New Status Page
  2. Name it and add your monitors
  3. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →