~4 min read • Updated Mar 10, 2026
1. Introduction: What Is Database Instrumentation?
Django provides a powerful mechanism for inspecting and controlling database queries by allowing developers to install wrapper functions around query execution. These wrappers can:
- Count the number of executed queries
- Measure query duration
- Log SQL statements and parameters
- Block database access in certain code paths
Wrappers behave similarly to middleware, but they are installed manually and apply only within a specific context manager.
2. How Query Wrappers Work
A wrapper is a callable that receives five arguments:
- execute — the callable that actually executes the query
- sql — the SQL string
- params — parameters for the SQL query
- many — whether the call is
execute()orexecutemany() - context — metadata including the connection and cursor
The wrapper is expected to call execute(sql, params, many, context) and return its result, optionally adding logic before or after the call.
3. Installing a Wrapper with execute_wrapper()
Wrappers are installed using a context manager:
from django.db import connection
with connection.execute_wrapper(wrapper):
# All queries inside this block are wrapped
do_something()
When the block exits, the wrapper is removed automatically.
4. Example: A Simple Query Blocker
def blocker(*args):
raise Exception("No database access allowed here.")
Usage in a view:
from django.db import connection
from django.shortcuts import render
def my_view(request):
context = {...}
template_name = ...
with connection.execute_wrapper(blocker):
return render(request, template_name, context)
Improved blocker with connection name:
def blocker(execute, sql, params, many, context):
alias = context["connection"].alias
raise Exception(f"Access to database '{alias}' blocked here")
5. Example: A Full Query Logger
import time
class QueryLogger:
def __init__(self):
self.queries = []
def __call__(self, execute, sql, params, many, context):
current_query = {"sql": sql, "params": params, "many": many}
start = time.monotonic()
try:
result = execute(sql, params, many, context)
except Exception as e:
current_query["status"] = "error"
current_query["exception"] = e
raise
else:
current_query["status"] = "ok"
return result
finally:
duration = time.monotonic() - start
current_query["duration"] = duration
self.queries.append(current_query)
Using the logger:
from django.db import connection
ql = QueryLogger()
with connection.execute_wrapper(ql):
do_queries()
print(ql.queries)
6. The execute_wrapper() Method
connection.execute_wrapper(wrapper) returns a context manager that:
- Installs the wrapper on entry
- Removes it on exit
- Applies only to the current thread’s connection
The wrapper must call execute() and return its result, unless intentionally blocking or modifying behavior.
7. Common Use Cases
- Ensuring no queries occur during template rendering (useful with prefetching)
- Logging all queries for debugging or performance analysis
- Measuring query execution time
- Preventing accidental database access in certain layers (e.g., enforcing “no DB in serializers”)
- Testing code paths that should not hit the database
Conclusion
Django’s execute_wrapper() provides a flexible and powerful way to monitor, log, or control database queries. By installing custom wrappers, developers can enforce architectural constraints, debug performance issues, or gather detailed query metrics—all without modifying Django’s core behavior.
Written & researched by Dr. Shahin Siami