fix(django): don't force ROOT_URLCONF import during django.setup() - #19467
Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits intoAug 5, 2026
Merged
Conversation
Codeowners resolved as |
Circular import analysis
|
BenchmarksBenchmark execution time: 2026-08-04 13:35:09 Comparing candidate commit ffbd66d in PR branch Found 0 performance improvements and 6 performance regressions! Performance is the same for 598 metrics, 10 unstable metrics. scenario:httppropagationinject-ids_only
scenario:iastaspects-upper_aspect
scenario:iastaspectsospath-ospathbasename_aspect
scenario:span-start
scenario:telemetryaddmetric-1-count-metric-1-times
scenario:tracer-small
|
Contributor
🎉 All green!🧪 All tests passed 🔗 Commit SHA: ffbd66d | Docs | Datadog PR Page | Give us feedback! |
christophe-papazian
marked this pull request as ready for review
August 4, 2026 13:04
christophe-papazian
requested review from
RamyElkest,
dubloom,
juanjux and
ncybul
August 4, 2026 13:04
Endpoint discovery walked the root resolver at the end of traced_populate, and reading resolver.url_patterns imports ROOT_URLCONF along with every view module behind it. Processes that never serve a request loaded that entire graph for nothing, costing one reporter 154MB of RSS per dramatiq worker. Move the walk to traced_load_middleware. Building a BaseHandler is the earliest reliable signal that a process will serve HTTP, so workers, management commands and cron jobs never import the URLconf, while gunicorn, uwsgi, daphne and runserver still report endpoints at startup rather than only after traffic lands. Wrap BaseHandler.load_middleware unconditionally so endpoint discovery no longer depends on DD_DJANGO_INSTRUMENT_MIDDLEWARE. Also add '@django' and '@Flask' to the telemetry suite in suitespec, which runs real apps of both but was only gated on shared contrib infrastructure. Fixes #19454 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scope the claims to what was measured. Django's own check_url_config imports the root URLconf whenever system checks run, so most manage.py invocations import it with or without ddtrace; the beneficiaries are processes that call django.setup() without running checks. The release note and the two test docstrings said "management commands and cron jobs", which was wrong for the common case, and the telemetry negative test used the help path only because it is one of the few command paths that skips checks -- it now bootstraps with a bare django.setup(), which is what a Celery or dramatiq worker actually does. Assert middleware wrapping state in both handler tests. Moving the wrapping under a runtime check meant the disabled-flag test would have passed even if wrapping had accidentally stayed on, and nothing pinned that wrapping still happens when the flag is at its default. Correct _collect_routes_once's docstring: the walk happens once per distinct resolver, not once per BaseHandler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…note Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
christophe-papazian
force-pushed
the
christophe-papazian/django-lazy-endpoint-discovery
branch
from
August 4, 2026 13:06
f0b229c to
ffbd66d
Compare
florentinl
approved these changes
Aug 4, 2026
dubloom
approved these changes
Aug 5, 2026
gh-worker-dd-mergequeue-cf854d
Bot
merged commit Aug 5, 2026
238e994
into
main
1272 of 1273 checks passed
gh-worker-dd-mergequeue-cf854d
Bot
deleted the
christophe-papazian/django-lazy-endpoint-discovery
branch
August 5, 2026 12:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
APPSEC-69515
Description
Alternative to #19465 for the same regression (#19454). Endpoint discovery walked the root resolver at the end of
traced_populate, sodjango.setup()importedROOT_URLCONFand every view module behind it — 154 MB of RSS perdramatiq worker that serves no requests. The eager call arrived in #17695, so every release from 4.9.0rc1 is affected.
This moves the walk to
traced_load_middleware: building aBaseHandleris the earliest reliable signal that aprocess will serve HTTP, so workers pay nothing while gunicorn/uwsgi/daphne still report endpoints at startup.
BaseHandler.load_middlewareis now wrapped unconditionally so discovery no longer depends onDD_DJANGO_INSTRUMENT_MIDDLEWARE.#19465 instead guards the walk on
ROOT_URLCONFalready being insys.modules, but Django imports the URLconfneither during
django.setup()nor duringWSGIHandler.__init__— only at the firstresolve(). Measured withget_wsgi_application(), which is what gunicorn imports:mainAlso adds
'@django'and'@flask'to thetelemetrysuite in suitespec, which runs real apps of both but wasgated only on shared contrib infrastructure — which is why #19465's CI is green while it deterministically fails
test_endpoint_discovery_event.Testing
test_setup_does_not_import_root_urlconfandtest_endpoint_discovery_skipped_without_http_handlerfail onmainand pass here.
test_endpoint_discovery_eventnow builds the WSGI application and still passes (it fails on #19465).test_wsgi_application_collects_endpointspins #17695'sinclude/test/prefix joining at startup, and a paired testpins that discovery survives
DD_DJANGO_INSTRUMENT_MIDDLEWARE=falsewhile middleware wrapping does not.On a bare
django.setup()(py3.13 / django 5.1):sys.modules1243 → 1220, live GC objects 109,218 → 106,123, maxRSS 82,884 → 81,496 KB. Small here only because the test app's view closure is small — the delta is that closure.
contrib::djangopasses except 5test_django_appsec_snapshots.pyfailures that reproduce identically onmain.Risks
Serving processes import the URLconf when the handler is built rather than on the first request — earlier than Django
would, but later and safer than the previous
Apps.populatetiming. Processes that build no handler no longer reportendpoints, which is the point. Management commands are unaffected either way: Django's own
check_url_configimportsthe URLconf whenever system checks run.
Additional Notes
Pre-existing and out of scope:
_collect_routes_oncemarks a resolver collected in itsfinallyblock even when thewalk raised; the
wsgi,appsec_integrations_djangoandddtracerunsuites have the same suitespec gap. ASGIcollection is reasoned but untested.
🤖 Generated with Claude Code
Credit to @bellini666 for diagnosing the regression and for
test_setup_does_not_import_root_urlconf, carried overfrom #19465.