Django error tracking with the Sentry SDK.
Django surfaces errors in several places at once: the request/response cycle, middleware, Celery tasks, ASGI consumers, and ORM queries. This guide covers how to wire up sentry-sdk[django] so every failure reaches your error tracker, what the DjangoIntegration captures without extra code, and the three gotchas that trip up every team on their first day.
20 seconds. Install sentry-sdk[django]. Call sentry_sdk.init at the top of settings.py with your DSN, DjangoIntegration(), and traces_sample_rate. Set DEBUG=False in production. Swap the DSN to your urgentry DSN and nothing else changes.
60 seconds. The DjangoIntegration hooks into Django’s middleware stack, signal system, and template engine automatically. Unhandled view exceptions, ORM query failures, and middleware errors all flow to your error tracker without extra code. What it does not capture: exceptions you catch and turn into 4xx responses, errors in Celery tasks unless you add CeleryIntegration, and anything that happens inside a WebSocket consumer unless you wrap the ASGI app with SentryAsgiMiddleware.
Three things will cause you grief on day one. First: DEBUG=True silences the SDK entirely (no events reach the server). Second: DisallowedHost exceptions from Host-header probes will flood your issue list if you do not filter them. Third: middleware order matters; the Sentry middleware must sit above everything else in MIDDLEWARE. All three are covered below, with fixes.
How Django errors actually surface
Django handles an HTTP request through a chain of middleware, then dispatches to a view function or class-based view, then passes the response back through the same middleware chain in reverse. When an exception escapes a view, Django’s exception middleware catches it and decides what to do: render a 404, render a 500, or re-raise.
The default 500 handler renders 500.html (or Django’s built-in debug page when DEBUG=True) and logs the traceback to the Python logger named django. That logger writes to wherever your logging config routes it, usually stderr or a file, which means the error is visible in your server logs but invisible to any error tracker unless you plumb it there yourself.
This is the gap the DjangoIntegration fills. It patches into Django’s exception handling path so that errors which would otherwise vanish into stderr instead get sent as structured events with full stack traces, request context, and user information.
Signals add a second surface. Django fires got_request_exception for unhandled view exceptions, but signal handlers themselves can raise exceptions too, and those are caught separately by the signal machinery. The integration hooks both paths. Template render errors (a TemplateDoesNotExist raised during response rendering) propagate up to the view layer and are captured the same way.
One surface the integration does not touch by default: management commands. If a handle() method raises, the process exits with a non-zero code and the traceback goes to stderr. If you run management commands in production (migrations, scheduled data jobs), add explicit try/except blocks with capture_exception or wrap the command with a decorator.
Install sentry-sdk[django]
The Django integration is part of the main sentry-sdk package, but the extras bracket pulls in optional dependencies for specific integrations. For Django, the relevant line is:
pip install "sentry-sdk[django]"
This installs sentry-sdk plus urllib3 for the HTTP transport. If you are also running Celery, add it to the same bracket:
pip install "sentry-sdk[django,celery]"
Version compatibility: sentry-sdk 1.x supports Django 3.2 through 5.x. As of mid-2026, the current stable release is in the 2.x series, which drops Python 3.7 and below but otherwise maintains the same integration API. Django 4.x and 5.x are both fully supported. Check the sentry-python changelog before upgrading in a major version jump; the DjangoIntegration constructor options have changed between 1.x and 2.x.
Pin to a specific minor version in your requirements file:
sentry-sdk[django]~=2.0
The tilde-equal constraint pins to >=2.0, <3.0. That gives you patch releases without pulling in a future major version that might change the API.
Wire up the SDK in settings.py
Call sentry_sdk.init at the top of your settings.py, before any Django app configuration. This ensures the SDK is active before Django loads models, registers signals, or imports views.
import os
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
integrations=[
DjangoIntegration(
transaction_style="url", # group transactions by URL pattern
middleware_spans=True, # create spans for each middleware
signals_spans=True, # create spans for each signal handler
cache_spans=True, # create spans for cache operations
),
],
environment=os.environ.get("DJANGO_ENV", "production"),
release=os.environ.get("APP_VERSION"),
traces_sample_rate=0.1,
send_default_pii=False, # True to include request body and user data
)
A few things to know about each parameter.
dsn: read from an environment variable, never hardcoded. The DSN contains a key that grants write access to your project. Commit it to source control and rotate it.
environment: tells the error tracker which deployment this event came from. Use distinct strings for production, staging, and development. Events from different environments group separately, which prevents staging noise from raising production alerts.
release: ties events to a specific deployment. Set this to your git SHA or a semantic version string from your CI pipeline. With a release set, urgentry can show you which deploy introduced a regression and track error frequency relative to release volume.
traces_sample_rate: controls what fraction of requests generate a performance transaction. At 1.0, every request creates a transaction and you will store a lot of data fast. At 0.1, one in ten requests is traced. Start low.
send_default_pii: when True, the SDK attaches the request body, cookies, and authenticated user information to events. Useful for debugging but carries GDPR implications. Keep it False until you have reviewed your data retention policy.
The DjangoIntegration constructor arguments control span granularity. transaction_style="url" is the right choice for most applications: it groups all requests to the same URL pattern together even when URL parameters differ. The alternative, "function_name", groups by the view function name, which can produce useful groupings in some architectures but becomes confusing with class-based views.
What DjangoIntegration captures automatically
Once initialized, the integration instruments Django without further configuration. Here is what it covers.
Unhandled view exceptions. Any exception that propagates out of a view function or class-based view and reaches Django’s exception middleware generates an event. The event includes the full stack trace, the HTTP method, the URL, the query string, and the request headers (minus cookies and auth tokens unless send_default_pii=True).
Middleware exceptions. The integration wraps each middleware in MIDDLEWARE with a span. If a middleware raises, the exception is captured before the middleware chain unwinds. This covers process_request, process_response, and process_exception hooks.
ORM query failures. A database error that propagates out of a view (a constraint violation, a connection failure, a bad query) is captured with the query attached as extra context. The integration does not log every query (that would be a performance disaster), but it does attach the failing query to the event when an ORM exception causes the request to fail.
Signal handler exceptions. Django signals fire synchronously during request handling. If a post_save or request_finished handler raises, the exception bubbles up into the request cycle and is captured. Signals fired outside the request cycle (from management commands, for example) are not captured unless you add explicit handling.
Template render errors. A TemplateDoesNotExist, TemplateSyntaxError, or an exception inside a template tag propagates up to the view and is captured. The event includes the template name and the tag that raised, which makes template errors far easier to diagnose than the raw traceback.
The integration also instruments the Django cache framework, the django.db connection for performance spans, and django.contrib.auth for user context on events. All of this happens through monkey-patching at init time, not through decorator-based instrumentation in your own code.
Capturing handled exceptions yourself
The integration captures exceptions that escape your code. For exceptions you catch and convert to user-facing error messages or business logic outcomes, you need to call capture_exception yourself.
import sentry_sdk
from django.http import JsonResponse
def payment_view(request):
try:
result = process_payment(request.POST)
except PaymentDeclinedError as e:
# This is an expected outcome, not a bug — do not capture
return JsonResponse({"error": "Payment declined"}, status=402)
except PaymentProviderError as e:
# This is our problem — capture it
sentry_sdk.capture_exception(e)
return JsonResponse({"error": "Payment service unavailable"}, status=503)
return JsonResponse({"status": "ok", "transaction_id": result.id})
The distinction matters. A user presenting a declined card is not a bug. A payment provider returning a 500 from their API is a bug in the integration between your system and theirs. Capture the second, not the first.
For attaching per-request context to a captured exception, use sentry_sdk.push_scope:
import sentry_sdk
def order_export_view(request, order_id):
try:
export = generate_export(order_id, format=request.GET.get("format"))
except ExportError as e:
with sentry_sdk.push_scope() as scope:
scope.set_tag("export.format", request.GET.get("format", "unknown"))
scope.set_extra("order_id", order_id)
scope.set_user({"id": str(request.user.id), "email": request.user.email})
sentry_sdk.capture_exception(e)
return HttpResponse("Export failed", status=500)
return export
The push_scope context manager creates a temporary scope for this capture only. Tags and extras set inside it do not leak to subsequent events on the same request. Use set_tag for indexed, filterable values (you will search by these). Use set_extra for arbitrary context you want visible in the event but do not need to filter by.
capture_message is available for events that are not exceptions. Use it for a rate limit warning, a business invariant violation that did not raise, or any condition you want to track without an exception object:
def subscribe_view(request):
if request.user.subscription_count >= 10:
sentry_sdk.capture_message(
"User exceeded subscription limit",
level="warning",
extras={"user_id": request.user.id, "count": request.user.subscription_count},
)
return HttpResponse("Limit reached", status=429)
# ... proceed
Celery + Django
Celery tasks run in a separate worker process. The DjangoIntegration does not cover them. You need to add CeleryIntegration and initialize the SDK inside the worker process.
# celery.py (your Celery application module)
import os
from celery import Celery
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.celery import CeleryIntegration
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
app = Celery("myproject")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
integrations=[
DjangoIntegration(),
CeleryIntegration(
monitor_beat_tasks=True, # report Celery Beat schedule failures
propagate_traces=True, # link task traces to the calling request trace
),
],
environment=os.environ.get("DJANGO_ENV", "production"),
release=os.environ.get("APP_VERSION"),
traces_sample_rate=0.1,
)
The CeleryIntegration patches task execution so that any exception raised inside a task body is captured before Celery marks the task as failed. The event includes the task name, the task ID, the queue, and the retry count.
The prefork worker pool creates a problem. When Celery uses prefork (the default), the main process forks child worker processes after the application initializes. A Sentry hub initialized in the main process is inherited by all forked children, but the hub’s internal transport connection state is tied to the parent’s file descriptors. Those descriptors do not survive a fork cleanly.
Fix this with the worker_process_init signal:
from celery.signals import worker_process_init
@worker_process_init.connect
def init_sentry(**kwargs):
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
integrations=[DjangoIntegration(), CeleryIntegration()],
environment=os.environ.get("DJANGO_ENV", "production"),
release=os.environ.get("APP_VERSION"),
traces_sample_rate=0.1,
)
This runs sentry_sdk.init in each forked worker process immediately after the fork. The child process gets a fresh hub with its own transport connection. The parent process also has the SDK initialized (from celery.py), but the parent does not execute tasks; only the children do.
If you use the solo pool (single-process, no forking), the prefork concern does not apply. Initialize once in celery.py and you are done.
Task retries interact with error tracking in an important way. By default, CeleryIntegration captures an event on every failed attempt, including retries. If a task retries five times before giving up, you get five events that all fingerprint to the same group. This is the right behavior in most cases: you want to know a task is failing on retry. If you only want the final failure, set capture_failed_retries=False in the CeleryIntegration constructor.
Channels + ASGI
Django 4+ ships first-class ASGI support. If you deploy with an ASGI server (Daphne, Uvicorn, Hypercorn) or use Django Channels for WebSocket support, the DjangoIntegration alone does not cover the ASGI layer. Add SentryAsgiMiddleware in your asgi.py:
# asgi.py
import os
from django.core.asgi import get_asgi_application
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_asgi_application()
application = SentryAsgiMiddleware(application)
If you use Django Channels, the routing setup goes through channels.routing.ProtocolTypeRouter. Wrap the entire router:
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
import myapp.routing
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(
URLRouter(myapp.routing.websocket_urlpatterns)
),
})
application = SentryAsgiMiddleware(application)
The SentryAsgiMiddleware wraps the outermost ASGI application. It catches any exception that escapes a consumer or HTTP handler and captures it before the ASGI server sees it. WebSocket consumers that raise in connect, receive, or disconnect are all covered.
One limitation: exceptions inside async consumers that are caught internally and turned into WebSocket close frames are not captured automatically. If your consumer logic catches exceptions and closes the connection gracefully, add explicit capture_exception calls in the except blocks.
Point the DSN at urgentry instead of Sentry
This is the part that requires the least explanation. In your urgentry instance, create a project and copy its DSN. It looks like:
https://<public_key>@errors.example.com/<project_id>
Set SENTRY_DSN to this value in your production environment. The sentry_sdk.init call in settings.py already reads from os.environ.get("SENTRY_DSN"). No code change is needed.
Everything the SDK does (the DjangoIntegration hooks, the CeleryIntegration task capture, capture_exception, push_scope, breadcrumbs) sends the same Sentry envelope protocol it always has. urgentry receives that envelope, parses it, and stores it. The DSN is the only difference between pointing at Sentry and pointing at urgentry.
urgentry runs as a single binary. It accepts the Sentry envelope protocol and OTLP/HTTP in the same process. If you have OpenTelemetry instrumentation alongside the Sentry SDK (a common pattern for distributed tracing), you can point both at the same urgentry instance. The DSN goes to the Sentry-compatible ingest endpoint; the OTLP spans go to port 4318. One server, two protocols.
The three Django gotchas
1. DisallowedHost noise
Django raises django.core.exceptions.DisallowedHost when an incoming request has a Host header that is not in ALLOWED_HOSTS. Scanners, bots, and load balancer health checks send malformed or empty Host headers constantly. If you deploy Django on a public-facing server, you will see dozens of DisallowedHost exceptions per hour within days of launch.
The DjangoIntegration captures these by default. Left unchecked, they will push real errors off your issue list.
Filter them with a before_send hook:
def before_send(event, hint):
if "exc_info" in hint:
exc_type, exc_value, tb = hint["exc_info"]
if exc_type.__name__ == "DisallowedHost":
return None # drop the event
return event
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
integrations=[DjangoIntegration()],
before_send=before_send,
# ... other options
)
before_send runs synchronously before the SDK sends any event. Returning None drops the event entirely. Returning the event (possibly mutated) sends it. Use this hook for any noise filtering you need: specific exception types, errors from known bot user-agents, or errors on paths you do not care about.
Also tighten ALLOWED_HOSTS. A DisallowedHost exception is a signal that something is probing your server with a Host header it should not be sending. Lock down the list to exactly the hostnames your application serves.
2. DEBUG=True suppresses all capture
When DEBUG=True, the DjangoIntegration does not send events. This is intentional: Django’s debug mode shows the full exception traceback in the browser, so sending the same information to an error tracker would be redundant and potentially noisy during development.
The consequence: if you test your error tracking setup with DEBUG=True, no events arrive. You check the urgentry UI, see nothing, and conclude the SDK is broken. It is not broken. Set DEBUG=False and provide a valid ALLOWED_HOSTS list before testing event delivery.
A pattern that helps during development: use a separate settings_test.py that sets DEBUG=False and points the DSN at a local urgentry instance. Run that settings module specifically for error tracking validation. Keep your main development settings with DEBUG=True for the browser error pages.
3. Middleware ordering
The DjangoIntegration patches Django’s middleware machinery at init time, which means it wraps whatever is in your MIDDLEWARE list. For the integration to capture exceptions from all middleware, including middleware that runs before your application logic, Sentry’s internal patching must be in place before Django processes any request.
The init call in settings.py handles this. The problem arises when you add a third-party middleware that installs itself at position 0 in MIDDLEWARE and catches exceptions before the Django exception machinery sees them. Some authentication and CORS middleware do this.
If you find that certain types of errors never appear in urgentry despite being raised, check whether a middleware in your stack is catching and swallowing them before the exception propagates to Django’s exception handler. Move any such middleware below your error-transparent middleware, or add explicit capture_exception calls inside the middleware that catches.
The sentry-sdk documentation recommends treating the Sentry init as the first thing that runs in settings.py, before importing Django modules. This is good advice and it sidesteps most ordering issues.
FAQ
Does DjangoIntegration capture all unhandled exceptions automatically?
It captures all exceptions that propagate out of a view, middleware, or signal handler without being caught. Exceptions you catch inside a view and convert to an HTTP response are not captured unless you call capture_exception explicitly. DEBUG=True also suppresses automatic capture entirely; only environments with DEBUG=False send events.
How do I stop DisallowedHost errors from flooding my issue tracker?
Add a before_send hook in sentry_sdk.init that checks hint["exc_info"][0].__name__ for DisallowedHost and returns None to drop the event. Also tighten ALLOWED_HOSTS to exactly the hostnames your app serves. A DisallowedHost error usually means a scanner is probing with an arbitrary Host header.
Does the Celery integration work with prefork workers?
Yes, but you must reinitialize the SDK in each forked worker process using the worker_process_init signal. Calling sentry_sdk.init only in the main process means forked workers inherit a stale hub with broken transport connections. The worker_process_init signal fires in each child after the fork and is the correct place to call init.
Can I use sentry-sdk[django] with Django Channels and ASGI?
Yes. Wrap your ASGI application with SentryAsgiMiddleware in asgi.py after creating it. WebSocket consumer exceptions and HTTP handler errors route through the ASGI middleware and are captured automatically. The DjangoIntegration handles traditional WSGI views; SentryAsgiMiddleware handles the async ASGI layer on top.
Does urgentry work with sentry-sdk[django] without code changes?
The only change is the DSN. Set the dsn parameter in sentry_sdk.init (or the SENTRY_DSN environment variable) to your urgentry project DSN. Every integration, every capture_exception call, and every SDK option works without modification. urgentry implements the same Sentry envelope ingest endpoints the SDK targets.
Sources
- Django middleware documentation — the canonical reference for how Django’s middleware chain processes requests and exceptions.
- sentry-sdk Django integration documentation — DjangoIntegration constructor options, supported Django versions, and the full list of auto-instrumented surfaces.
- Celery signals documentation —
worker_process_initsignal and other worker lifecycle hooks referenced in the prefork section. - sentry-python license — the MIT license governing the
sentry-sdkclient library. - urgentry SDK setup documentation — DSN creation, project setup, and the OTLP endpoint for mixed Sentry/OTel deployments.
- FSL-1.1-Apache-2.0 license text — the FSL license under which urgentry is distributed.
Ready to wire up your Django app?
urgentry runs as a single binary, accepts the Sentry SDK out of the box, and takes under ten minutes to start receiving events. Your sentry_sdk.init call stays exactly as written above. Only the DSN changes.