Actions¶
Actions are triggered when a budget constraint is violated. They determine what happens when a query exceeds the budget.
Built-in actions¶
LOG¶
Logs a warning via the django.query_budget logger:
WARNING Query budget violated: Total runtime 35.2s exceeds limit of 30.0s [scope=default, action=LOG]
The query proceeds normally. This is the default action.
To capture these in your logging configuration:
LOGGING = {
"loggers": {
"django.query_budget": {
"level": "WARNING",
"handlers": ["console"],
},
},
}
REJECT¶
Raises QueryBudgetExceeded before the next query executes:
from django_query_budget import QueryBudgetExceeded
try:
MyModel.objects.all()
except QueryBudgetExceeded as e:
print(e.violation.message)
print(e.violation.constraint_name) # "total_runtime", "max_queries", etc.
QueryBudgetExceeded is a subclass of Exception (not DatabaseError) to avoid confusing Django’s transaction management.
Important: REJECT prevents the next query after the budget is exceeded. The query that pushed the budget over the limit has already completed. For max_single_query, the slow query has already finished — the action fires after the fact.
Sync vs async actions¶
Actions support two execution modes:
SYNC — Executes inline on the calling thread. The query waits for the action to complete. Required for actions that need to affect control flow (like REJECT raising an exception).
ASYNC — Enqueued to a background thread. The query proceeds immediately without waiting. Ideal for I/O-bound actions like sending alerts.
Built-in actions (LOG, REJECT) are synchronous. Custom actions default to async — this prevents slow actions (like sending a Slack message) from adding latency to every query.
Custom actions¶
Register custom actions with register_action:
from django_query_budget import register_action
def slack_alert(budget, tracker, violation):
"""Send a Slack alert when the budget is exceeded."""
send_slack_message(
channel="#db-alerts",
text=f"Query budget exceeded: {violation.message}",
)
# Custom actions are async by default — this won't block queries
register_action("SLACK_ALERT", slack_alert)
To make a custom action synchronous:
from django_query_budget import register_action, ExecutionMode
def block_and_alert(budget, tracker, violation):
"""Synchronous action that must complete before the query proceeds."""
...
register_action("BLOCK_AND_ALERT", block_and_alert, mode=ExecutionMode.SYNC)
Then use it in your configuration:
QUERY_BUDGET = {
"default": {
"total_runtime": "30m",
"window": "5m",
"action": "SLACK_ALERT",
},
}
Action signature¶
def my_action(budget: Budget, tracker: BudgetTracker, violation: Violation) -> None:
...
Parameters:
budget— TheBudgetthat was violatedtracker— TheBudgetTrackerwith current statsviolation— AViolationdataclass with details:constraint_name—"total_runtime","max_queries", or"max_single_query"current_value— The measured valuelimit_value— The budget limitfingerprint— The query fingerprint (formax_single_queryviolations)message— Human-readable description
Note
Async actions share the same bounded queue and background thread as async hooks. If the queue is full (default 10,000), the action is dropped and a counter is incremented.
Custom constraints¶
You can add custom constraints that are checked alongside the built-in ones:
from django_query_budget.constraints import Violation
def max_slow_queries(tracker, budget):
"""Fail if more than 3 queries took over 1 second."""
slow_count = sum(
1 for entry in tracker._entries
if entry.duration > 1.0
)
if slow_count > 3:
return Violation(
constraint_name="max_slow_queries",
current_value=slow_count,
limit_value=3,
fingerprint=None,
message=f"{slow_count} slow queries (>1s) exceeds limit of 3",
)
return None
# Pass constraints when creating a budget
@query_budget(
total_runtime="1h",
window="5m",
constraints=[max_slow_queries],
)
def my_view(request):
...