Cluster Sync¶
By default, Django Query Budget tracks budgets per-process. For multi-process or multi-node deployments, you can enable cluster-wide budget sharing via a sync backend.
Architecture¶
The sync system is local-first:
Each process maintains its own in-memory trackers
Enforcement always happens locally (no external calls on the query hot path)
A background thread periodically pushes local stats and pulls the cluster-wide view
The merged cluster view is used for enforcement
This means budgets are eventually consistent across the cluster. A single process could temporarily exceed its share of the budget before sync catches up. This is by design — the library is a budget/circuit-breaker, not a billing system.
Redis backend¶
The recommended backend for production use:
QUERY_BUDGET = {
"default": {
"total_runtime": "30m",
"window": "5m",
"action": "LOG",
},
"sync": {
"backend": "django_query_budget.sync.redis.RedisSyncBackend",
"url": "redis://localhost:6379/0",
"interval": 5,
},
}
How it works¶
Push: Each node writes its stats to a Redis hash using
HSET. Key format:query_budget:{scope}:{window_bucket}. Each node’s stats are stored under itsnode_idfield.Pull:
HGETALLreads all nodes’ stats, then sums them for the cluster-wide view.TTL: Keys expire automatically after the window duration plus a buffer.
No double-counting: each node’s contribution is stored separately (not merged into a single counter).
Database backend¶
For deployments without Redis:
QUERY_BUDGET = {
"default": {
"total_runtime": "30m",
"window": "5m",
"action": "LOG",
},
"sync": {
"backend": "django_query_budget.sync.db.DatabaseSyncBackend",
"interval": 10,
},
}
This uses a BudgetSnapshot Django model to store stats. Run migrations after enabling:
python manage.py migrate query_budget
How it works¶
Push:
update_or_createper nodePull: Aggregates across all nodes for each scope
Cleanup: Expired rows should be cleaned up periodically (management command or scheduled task)
Note
The database backend adds database queries for sync operations. Use a longer interval (e.g., 10-30 seconds) to minimize overhead.
Custom backends¶
Implement BaseSyncBackend:
from django_query_budget.sync.base import BaseSyncBackend
from django_query_budget.tracker import BudgetStats
class MyBackend(BaseSyncBackend):
def push(self, scope_key: str, stats: BudgetStats) -> None:
"""Write this node's stats to the shared store."""
...
def pull(self, scope_key: str) -> BudgetStats | None:
"""Read the merged cluster-wide stats."""
...
def clear(self, scope_key: str) -> None:
"""Remove stats for a scope."""
...
Then configure it:
QUERY_BUDGET = {
"sync": {
"backend": "myapp.sync.MyBackend",
"interval": 5,
},
}
Sync interval¶
The interval setting controls how often the background thread runs (in seconds). Trade-offs:
Interval |
Consistency |
Overhead |
|---|---|---|
1s |
Near-real-time |
Higher |
5s (default) |
Good enough for most cases |
Low |
30s |
Significant lag |
Minimal |
Disabling sync¶
If sync is not configured, everything is process-local. This is the default and is perfectly valid for single-process deployments (e.g., development, small apps).