Database Instrumentation¶
HyperDjango provides built-in tools for monitoring database performance: per-request query tracking, slow query detection, N+1 pattern alerts, connection pool health checks, and request profiling with nanosecond precision.
PerformanceMiddleware¶
The central component for query monitoring. It tracks every SQL query per request, detects slow queries and N+1 patterns, and exposes a dashboard.
from hyperdjango.performance import PerformanceMiddleware
perf = PerformanceMiddleware(
slow_query_threshold_ms=100, # Queries over 100ms are flagged (default: 100)
n_plus_one_threshold=5, # Same SQL repeated 5+ times = N+1 alert (default: 5)
max_history=1000, # Ring buffer size for request stats (default: 1000)
dashboard_path="/debug/performance", # Dashboard URL (default)
)
app.use(perf)
Response Headers¶
Every response includes instrumentation headers:
These headers tell you at a glance how many queries a request executed and how long they took in total.
Recording Queries¶
The middleware hooks into the database layer. Each query execution is recorded with:
- The normalized SQL text
- Duration in milliseconds
- Timestamp
# The middleware records queries automatically. For manual recording:
perf.record_query("SELECT * FROM users WHERE id = $1", duration_ms=2.3)
Aggregate Statistics¶
Access aggregate stats programmatically:
stats = perf.get_stats()
# Returns:
# {
# "total_requests": 15432,
# "total_queries": 89201,
# "avg_queries_per_request": 5.8,
# "slow_query_count": 23,
# "n_plus_one_count": 7,
# "recent_slow_queries": [
# {"sql": "SELECT ... FROM orders WHERE ...", "duration_ms": 234.5},
# ...
# ],
# "recent_n_plus_one": [
# "SELECT * FROM order_items WHERE order_id = $1",
# ...
# ],
# }
Slow Query Detection¶
PerformanceMiddleware Threshold¶
Queries exceeding slow_query_threshold_ms are flagged and tracked in the recent history. The default threshold is 100ms.
SlowQueryLog (Persistent)¶
For production, use SlowQueryLog to persist slow queries in a PostgreSQL UNLOGGED table. This survives process restarts and provides a queryable history.
from hyperdjango.pool import SlowQueryLog
slow_log = SlowQueryLog(db, threshold_ms=100)
await slow_log.ensure_table()
# The table created:
# CREATE UNLOGGED TABLE hyper_slow_queries (
# id SERIAL PRIMARY KEY,
# sql_text TEXT NOT NULL,
# duration_ms REAL NOT NULL,
# params_summary TEXT,
# timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
# )
The UNLOGGED table avoids WAL overhead -- slow query logging does not add write-amplification to your production database.
Indexes are created on timestamp DESC and duration_ms DESC for efficient querying:
-- Find the 10 slowest queries in the last hour
SELECT sql_text, duration_ms, timestamp
FROM hyper_slow_queries
WHERE timestamp > NOW() - INTERVAL '1 hour'
ORDER BY duration_ms DESC
LIMIT 10;
N+1 Query Detection¶
The PerformanceMiddleware detects N+1 patterns by normalizing SQL and counting repeated patterns within a single request. When the same query pattern appears more than n_plus_one_threshold times (default: 5), it is flagged.
# This triggers an N+1 alert:
users = await User.objects.all()
for user in users:
# Each iteration runs: SELECT * FROM orders WHERE user_id = $1
orders = await Order.objects.filter(user_id=user.id).all()
# Fix with prefetch_related or select_related:
users = await User.objects.prefetch_related("orders").all()
N+1 patterns appear in:
- The performance dashboard
perf.get_stats()["recent_n_plus_one"]- The JSON stats endpoint at
{dashboard_path}/json
QueryTimer¶
QueryTimer wraps db.query and db.execute to automatically time every query and log slow ones.
from hyperdjango.pool import QueryTimer, SlowQueryLog
slow_log = SlowQueryLog(db, threshold_ms=100)
await slow_log.ensure_table()
timer = QueryTimer(db, slow_log=slow_log, threshold_ms=100)
timer.install() # Patches db.query and db.execute
# Now all queries are automatically timed.
# Slow queries are written to the hyper_slow_queries table.
# Graceful shutdown: wait for in-flight queries
await timer.drain(timeout_seconds=30)
Performance Dashboard¶
The PerformanceMiddleware serves an HTML dashboard at the configured path (default: /debug/performance).
GET /debug/performance -> HTML dashboard with charts and tables
GET /debug/performance/json -> JSON stats for programmatic access
The dashboard shows:
- Total requests and queries
- Average queries per request
- Slow query count and list (sorted by duration)
- N+1 pattern detections
- Recent request timeline
JSON Endpoint¶
{
"total_requests": 15432,
"total_queries": 89201,
"avg_queries_per_request": 5.8,
"slow_query_count": 23,
"n_plus_one_count": 7,
"recent_slow_queries": [...],
"recent_n_plus_one": [...]
}
Request Profiling¶
For detailed per-request breakdown, use the profiling system which provides nanosecond-precision timing via the native Zig extension.
from hyperdjango.profiling import profile_handler
# Profile a single route
@app.route("GET", "/users")
@profile_handler
async def list_users(request):
users = await User.objects.all()
return Response.json([u.to_dict() for u in users])
Profiled requests include an X-Profile header with timing breakdown:
App-Wide Profiling¶
Enable profiling for all routes:
This adds the X-Profile header to every response without code changes to individual handlers.
Connection Pool Monitoring¶
PoolHealthChecker¶
Periodically validates connections in the pool and removes dead ones.
from hyperdjango.pool import PoolHealthChecker
checker = PoolHealthChecker(db, interval_seconds=30)
# Manual check
await checker.check()
# Background periodic checks
checker.start()
# Stop on shutdown
checker.stop()
Pool Stats¶
The database connection pool exposes statistics for monitoring:
stats = db.pool_stats()
# Returns pool utilization metrics:
# - total connections
# - idle connections
# - active (in-use) connections
# - wait queue depth
Use these stats in health check endpoints or monitoring dashboards.
Putting It All Together¶
A production setup combining all instrumentation:
from hyperdjango import HyperApp
from hyperdjango.database import Database
from hyperdjango.performance import PerformanceMiddleware
from hyperdjango.pool import SlowQueryLog, QueryTimer, PoolHealthChecker
app = HyperApp("myapp")
@app.on_startup
async def setup():
db = Database("postgres://localhost/mydb")
await db.connect()
app.db = db
# Persistent slow query log
slow_log = SlowQueryLog(db, threshold_ms=100)
await slow_log.ensure_table()
# Auto-timing on all queries
timer = QueryTimer(db, slow_log=slow_log, threshold_ms=100)
timer.install()
# Connection health checks every 30 seconds
checker = PoolHealthChecker(db, interval_seconds=30)
checker.start()
# Per-request query tracking + N+1 detection + dashboard
app.use(PerformanceMiddleware(
slow_query_threshold_ms=100,
n_plus_one_threshold=5,
dashboard_path="/debug/performance",
))
# Enable profiling in development
if app.debug:
app.profiling = True
See Also¶
- Performance Guide -- optimization strategies
- Pool -- connection pool configuration and optimization
- Profiling -- detailed request profiling documentation
- Database -- connection pool, transactions, and query execution
- Logging -- structured logging for query events