Hooks

Hooks provide extension points for observability and custom logic. They fire at key moments in the query lifecycle without blocking query execution (by default).

Hook events

on_query_executed

Fires after every query completes:

from django_query_budget import register_hook

def log_slow_queries(fingerprint, duration, tracker, **kwargs):
    if duration > 1.0:
        print(f"Slow query ({duration:.2f}s): {fingerprint}")

register_hook("on_query_executed", log_slow_queries)

Parameters: fingerprint, duration, tracker

on_budget_violation

Fires when any constraint is breached, before the action:

def send_metrics(budget, tracker, violation, **kwargs):
    statsd.increment("query_budget.violation", tags=[
        f"constraint:{violation.constraint_name}",
        f"scope:{budget.name or 'default'}",
    ])

register_hook("on_budget_violation", send_metrics)

Parameters: budget, tracker, violation

on_sync

Fires after each sync cycle (only relevant with cluster sync):

def log_cluster_stats(scope_key, local_stats, cluster_stats, **kwargs):
    if cluster_stats:
        print(f"Cluster total runtime for {scope_key}: {cluster_stats.total_runtime:.1f}s")

register_hook("on_sync", log_cluster_stats)

Parameters: scope_key, local_stats, cluster_stats

Execution modes

Each hook runs in one of two modes:

ASYNC (default)

Hooks are enqueued onto a bounded queue and executed by a dedicated background thread. This is the safe default — hooks never add latency to query execution.

from django_query_budget import register_hook

# Async by default
register_hook("on_query_executed", my_callback)

If the queue is full (default max: 10,000), new events are dropped. A counter tracks dropped events.

SYNC

Hooks execute inline on the calling thread. Use this only when the hook must run before the query lifecycle continues:

from django_query_budget import register_hook, ExecutionMode

register_hook("on_query_executed", my_callback, mode=ExecutionMode.SYNC)

Warning

Sync hooks add latency to every query. Keep them fast and never do I/O in a sync hook.

Class-based hooks

For hooks that need initialization or state:

from django_query_budget import BaseHook, ExecutionMode

class MetricsHook(BaseHook):
    mode = ExecutionMode.ASYNC

    def __init__(self, statsd_client):
        self.client = statsd_client

    def __call__(self, fingerprint, duration, tracker, **kwargs):
        self.client.timing("query.duration", duration * 1000)

hook = MetricsHook(statsd_client=my_statsd)
register_hook("on_query_executed", hook)

Error handling

Hook exceptions are caught and logged via the django.query_budget.hooks logger. They never propagate to the caller or affect query execution.

LOGGING = {
    "loggers": {
        "django.query_budget.hooks": {
            "level": "ERROR",
            "handlers": ["console"],
        },
    },
}