Framework Guide

Python API Monitoring

Set up health check endpoints and uptime monitoring for your Python web application. Examples for Flask, FastAPI, and Django.

Why Monitor Your Python App?

Python web applications can fail in subtle ways that are hard to detect without external monitoring. A WSGI server might appear running while the application itself is stuck, or a memory leak in a long-running process can gradually degrade performance until the app becomes unresponsive.

  • GIL contention -- CPU-bound tasks can block all threads in a Python process, making the app unresponsive
  • Memory leaks -- Long-running Python processes can accumulate objects that the garbage collector never frees
  • Worker exhaustion -- Gunicorn or uWSGI workers can all become busy with slow requests, leaving no capacity for new ones
  • Dependency failures -- Database, Redis, or third-party API outages cascade through your application

Flask Health Endpoint

from flask import Flask, jsonify
from datetime import datetime

app = Flask(__name__)

@app.route('/health')
def health():
    return jsonify({
        'status': 'healthy',
        'timestamp': datetime.utcnow().isoformat()
    })

@app.route('/ready')
def ready():
    checks = {}

    # Check database
    try:
        db.session.execute('SELECT 1')
        checks['database'] = 'ok'
    except Exception as e:
        checks['database'] = str(e)
        return jsonify({'status': 'unhealthy', 'checks': checks}), 503

    return jsonify({'status': 'healthy', 'checks': checks})

FastAPI Health Endpoint

from fastapi import FastAPI, Response
from datetime import datetime
from pydantic import BaseModel

app = FastAPI()

class HealthResponse(BaseModel):
    status: str
    timestamp: str

@app.get("/health", response_model=HealthResponse)
async def health():
    return {
        "status": "healthy",
        "timestamp": datetime.utcnow().isoformat()
    }

@app.get("/ready")
async def ready(response: Response):
    checks = {}
    healthy = True

    # Check database
    try:
        await database.execute("SELECT 1")
        checks["database"] = "ok"
    except Exception as e:
        checks["database"] = str(e)
        healthy = False

    if not healthy:
        response.status_code = 503

    return {
        "status": "healthy" if healthy else "unhealthy",
        "checks": checks
    }

Django Health Endpoint

# views.py
from django.http import JsonResponse
from django.db import connection
from datetime import datetime

def health(request):
    return JsonResponse({
        'status': 'healthy',
        'timestamp': datetime.utcnow().isoformat()
    })

def ready(request):
    checks = {}

    # Check database
    try:
        with connection.cursor() as cursor:
            cursor.execute('SELECT 1')
        checks['database'] = 'ok'
    except Exception as e:
        checks['database'] = str(e)
        return JsonResponse({
            'status': 'unhealthy',
            'checks': checks
        }, status=503)

    return JsonResponse({
        'status': 'healthy',
        'checks': checks
    })

# urls.py
urlpatterns = [
    path('health/', views.health),
    path('ready/', views.ready),
]

Comprehensive Health Check

import psutil
import time
from datetime import datetime

def comprehensive_health():
    start_time = time.time()
    health = {
        'status': 'healthy',
        'timestamp': datetime.utcnow().isoformat(),
        'checks': {},
        'system': {
            'cpu_percent': psutil.cpu_percent(),
            'memory_percent': psutil.virtual_memory().percent,
            'disk_percent': psutil.disk_usage('/').percent
        }
    }

    # Check database
    try:
        db_start = time.time()
        db.session.execute('SELECT 1')
        health['checks']['database'] = {
            'status': 'ok',
            'response_time_ms': (time.time() - db_start) * 1000
        }
    except Exception as e:
        health['status'] = 'unhealthy'
        health['checks']['database'] = {
            'status': 'error',
            'message': str(e)
        }

    # Check Redis
    try:
        redis_start = time.time()
        redis_client.ping()
        health['checks']['redis'] = {
            'status': 'ok',
            'response_time_ms': (time.time() - redis_start) * 1000
        }
    except Exception as e:
        health['status'] = 'unhealthy'
        health['checks']['redis'] = {
            'status': 'error',
            'message': str(e)
        }

    health['response_time_ms'] = (time.time() - start_time) * 1000
    return health

Best Practices

  • Use async where possible — Async health checks won't block your event loop
  • Add timeouts — Use asyncio.wait_for() or signal.alarm()
  • Return proper status codes — 200 for healthy, 503 for unhealthy
  • Include version info — Helps debug deployment issues

Common Python Production Issues

Worker Timeout Under Load

Gunicorn workers have a default timeout of 30 seconds. If a request takes longer (large file uploads, slow database queries), the worker is killed and restarted. Health checks with timeouts detect when all workers are stuck processing slow requests.

Memory Growth in Long-Running Processes

Python processes can slowly consume memory due to circular references, large caches, or C extension leaks. Monitor psutil.virtual_memory().percent in your health check and configure Gunicorn's --max-requests to recycle workers periodically.

Database Connection Pool Exhaustion

SQLAlchemy and Django ORM maintain connection pools. Under sustained load, all connections can become occupied, causing new queries to queue or timeout. Include a database ping in your readiness check to catch pool exhaustion early.

WSGI/ASGI Server Crashes

Segfaults in C extensions or out-of-memory kills can crash your Python process. Use a process manager (systemd, supervisord) to auto-restart, and external monitoring to detect the downtime gap between crash and recovery.

Frequently Asked Questions

What should a Python health check endpoint return?
A health check should return HTTP 200 with a JSON body containing at minimum a status field. For comprehensive checks, include database connectivity, cache status, system metrics via psutil (CPU, memory, disk), and app version. Return 503 if any critical dependency is unhealthy.
Should I use Flask, FastAPI, or Django for my health check?
Use whichever framework your application already runs. Flask is simplest for basic apps, FastAPI is ideal for async APIs with automatic OpenAPI docs, and Django is best for full-featured web applications. The health check pattern is similar across all three -- a GET endpoint returning JSON with a status field.
How do I monitor a Python API behind a reverse proxy?
Configure your reverse proxy (Nginx, Caddy) to pass health check requests through to your Python app. Alternatively, expose the health endpoint on a separate port that bypasses the proxy. UptimeSignal checks the public URL, so ensure the /health path is accessible externally without authentication.
Should I use sync or async health checks in Python?
Use async if your framework supports it (FastAPI, async Django). Async health checks won't block other requests while waiting for database or cache responses. For Flask with Gunicorn, sync is fine since each worker handles one request at a time. Always add timeouts with asyncio.wait_for() or signal.alarm() to prevent slow checks from hanging.
How often should I check my Python API health?
For production APIs, check every 1-5 minutes. UptimeSignal's free tier checks every 5 minutes, which catches most outages quickly. Pro tier supports 1-minute intervals for faster detection. Avoid checking more frequently than every 30 seconds to prevent unnecessary load on your WSGI/ASGI server.

Monitor your Python API

Add your health endpoint to UptimeSignal and get alerted when it fails. Free tier includes 25 monitors.

Start monitoring free →

No credit card required. Commercial use allowed.

More Framework Guides