Profiling Django Apps: Debug Toolbar, Silk & Beyond

Blog / MongoDB · June 19, 2017 · Updated June 10, 2026 · 11 min read
Profiling Django Apps: Debug Toolbar, Silk & Beyond

To profile a Django application in 2026, you instrument three layers: use django-debug-toolbar in development to count SQL queries and catch N+1 problems, use django-silk to persist per-request SQL and Python cProfile data in a browsable UI, and run an APM such as Sentry Performance, Scout, or New Relic in production for distributed tracing. Most Django slowness is database-bound, so a query profiler is almost always the first tool you reach for.

This post modernizes an older guide that focused on the django-web-profiler package. That package still works as a request/SQL logger, but it is no longer actively maintained, so we cover it briefly for completeness and then pivot to the tools real teams rely on today.

Key takeaways

  • Start in development with django-debug-toolbar. Its SQL panel shows the exact query count and timing for each request, which is the fastest way to spot N+1 query explosions.
  • Use django-silk when you need to keep the data. Silk persists every request's SQL and cProfile output to a real UI at /silk/, so it works for staging and shared environments, not just your laptop.
  • The N+1 query problem is the #1 Django performance issue. Fix it with select_related() and prefetch_related() — see our Django database access optimization guide.
  • Never run debug-toolbar in production. It is dev-only by design. For production, use a sampling profiler like py-spy or an APM with a low traces_sample_rate.
  • Profile Python code, not just SQL, with cProfile/pstats, line_profiler, and memory tools like memory_profiler/tracemalloc.
  • django-web-profiler is unmaintained — prefer the tools below for new projects.

What does it mean to profile a Django application?

Profiling means measuring where a request spends its time and resources so you can optimize the parts that actually matter. For a typical Django request, the things worth measuring are:

  • Database: how many SQL queries run, how long they take, and whether any are duplicated (the N+1 signature).
  • Python/CPU time: which view, serializer, or template-rendering function dominates wall-clock time.
  • Memory: allocations and leaks across a request or a long-running worker.
  • External calls: cache hits/misses, HTTP calls to third-party APIs, and queue/task latency.

The mistake teams make is guessing. Profile first, then optimize the hotspot — usually the database.

Which Django profiling tool should I use?

There is no single tool that does everything well across dev and prod. The table below compares the options most Django teams use in 2026 so you can pick the right one for each environment.

Tool What it measures Dev or prod Overhead Notes
django-debug-toolbar SQL queries + timing, templates, cache, signals, request time Dev only High (in-page panels) The default first step; never enable in prod
django-silk Per-request SQL + Python cProfile, persisted to a UI at /silk/ Dev / staging Medium-high Stores results in DB; good for shared environments
cProfile + pstats Function-level CPU time for any Python code Dev (ad hoc) / prod (targeted) Medium Built into the stdlib; deterministic profiler
py-spy Sampling CPU profiler, attaches to a running PID Dev / prod Very low No code change; safe to point at a live process
line_profiler Line-by-line timing of a decorated function Dev High on target fn Great for one slow function
memory_profiler / tracemalloc Memory allocations and leaks Dev / prod Medium tracemalloc ships with Python
Sentry Performance Distributed traces, slow transactions, N+1 detection Prod (sampled) Low (sampled) Set traces_sample_rate low in prod
Scout APM / New Relic / Datadog End-to-end APM: transactions, DB, external calls Prod Low (sampled) Continuous monitoring + alerting

How do I set up django-debug-toolbar?

The django-debug-toolbar is the dev-time standard and the first thing to reach for. It renders panels alongside your pages: an SQL panel that counts every query and flags duplicates, plus time, templates, cache, and signals panels.

Install it, then wire up three settings — INSTALLED_APPS, MIDDLEWARE, and INTERNAL_IPS:

pip install django-debug-toolbar
# settings.py (development only)

INSTALLED_APPS = [
    # ...
    "django.contrib.staticfiles",  # required
    "debug_toolbar",
]

MIDDLEWARE = [
    # DebugToolbarMiddleware should come as early as possible,
    # but after any encoding/gzip middleware.
    "debug_toolbar.middleware.DebugToolbarMiddleware",
    # ...
]

# The toolbar only renders for IPs in this list.
INTERNAL_IPS = [
    "127.0.0.1",
]
# urls.py
from django.conf import settings
from django.urls import include, path

urlpatterns = [
    # ... your urls ...
]

if settings.DEBUG:
    import debug_toolbar

    urlpatterns += [
        path("__debug__/", include("debug_toolbar.urls")),
    ]

Now load any page with DEBUG = True and open the SQL panel. If a list view that shows 50 rows fires 51 queries, you have found an N+1 problem. The toolbar is strictly development-only — it exposes internals and slows responses, so it must never ship to production.

How do I profile with django-silk?

django-silk picks up where the toolbar stops: it persists profiling data. Every request's SQL queries and timings are recorded to your database and shown in a browsable UI at /silk/, so it works on staging and other shared, production-like environments — not just your local machine. It also integrates Python's cProfile so you can profile specific code paths per request.

Install and configure it:

pip install django-silk
# settings.py

INSTALLED_APPS = [
    # ...
    "silk",
]

MIDDLEWARE = [
    "silk.middleware.SilkyMiddleware",
    # ...
]

# Enable per-request Python profiling (cProfile under the hood).
SILKY_PYTHON_PROFILER = True
SILKY_PYTHON_PROFILER_BINARY = True  # save .prof files you can open in snakeviz
# urls.py
from django.urls import include, path

urlpatterns = [
    # ...
    path("silk/", include("silk.urls", namespace="silk")),
]

# Then run migrations so Silk can store request data:
#   python manage.py migrate

Visit /silk/ to see every request ranked by time and query count. You can also profile a single block of code with the silk_profile context manager / decorator, which records a labelled cProfile span:

from silk.profiling.profiler import silk_profile


@silk_profile(name="Generate monthly report")
def generate_report(month):
    # ... expensive work ...
    return report


# Or as a context manager around a hot section:
with silk_profile(name="Serialize dashboard payload"):
    data = serialize_dashboard(user)

What is the N+1 query problem and how do I fix it?

The N+1 query problem is the single most common Django performance bug. It happens when you fetch a list of N objects with one query, then trigger one extra query per object while accessing a related field in a loop or template — N+1 queries total.

The SQL panel in debug-toolbar (or the request view in Silk) makes it obvious: you see the same query repeated dozens of times. Here is the classic mistake and its fix:

# BAD: 1 query for books + 1 query per book for its author = N+1
books = Book.objects.all()
for book in books:
    print(book.author.name)  # hits the DB every iteration

# GOOD: foreign key / one-to-one -> JOIN with select_related (2 -> 1 query path)
books = Book.objects.select_related("author")
for book in books:
    print(book.author.name)  # no extra queries

# GOOD: reverse FK / many-to-many -> prefetch_related (a second batched query)
authors = Author.objects.prefetch_related("books")
for author in authors:
    print(author.name, [b.title for b in author.books.all()])

Use select_related() for forward ForeignKey/OneToOne relations (it adds a SQL JOIN) and prefetch_related() for reverse foreign keys and many-to-many relations (it runs one extra batched query and joins in Python). For a deeper treatment with annotate, only/defer, and Prefetch objects, read our Django database access optimization guide. When you need to inspect or hand-tune the generated SQL, see writing raw SQL queries in Django.

How do I see the raw SQL Django generates?

Before reaching for a package, Django itself can show you the queries. With DEBUG = True, connection.queries holds every SQL statement and its timing for the current request:

from django.db import connection, reset_queries

reset_queries()
list(Book.objects.select_related("author")[:50])  # run your code

print(len(connection.queries), "queries")
for q in connection.queries:
    print(q["time"], q["sql"][:120])

# Inspect the SQL a queryset WOULD run, without executing it:
print(str(Book.objects.filter(author__name="Ada").query))

# Ask the database for its plan:
#   Book.objects.filter(...).explain(analyze=True)

For database-side profiling, pair QuerySet.explain(analyze=True) (which runs EXPLAIN ANALYZE) with your database's slow-query log (e.g. PostgreSQL log_min_duration_statement or MySQL slow_query_log) to find expensive queries in production traffic without touching application code.

How do I profile Python CPU and memory?

When the bottleneck is CPU-bound Python — heavy serialization, report generation, image processing — profile the code itself.

cProfile is the deterministic profiler in the standard library. Wrap any callable and sort the stats by cumulative time:

import cProfile
import pstats
import io


def profile(func, *args, **kwargs):
    pr = cProfile.Profile()
    pr.enable()
    result = func(*args, **kwargs)
    pr.disable()

    s = io.StringIO()
    pstats.Stats(pr, stream=s).sort_stats("cumulative").print_stats(20)
    print(s.getvalue())
    return result


# Usage:  profile(generate_report, month="2026-05")
# Or profile a management command from the shell:
#   python -m cProfile -o out.prof manage.py my_command
#   then open out.prof in snakeviz for a flame view.

For finer detail, line_profiler times a function line by line (decorate it with @profile and run via kernprof -l), while memory_profiler and the stdlib tracemalloc module track allocations to find leaks in long-running workers.

How do I profile Django in production safely?

Production needs low-overhead, sampled tooling — never debug-toolbar. Two complementary approaches:

1. py-spy — a sampling profiler that needs no code change. It attaches to a running process by PID and samples the call stack, so it is safe to point at a live web or worker process. Generate a flame graph or a live top view:

pip install py-spy

# Live, htop-style view of where a running process spends time:
py-spy top --pid 12345

# Sample for 60s and write an interactive flame graph:
py-spy record -o profile.svg --pid 12345 --duration 60

2. An APM for continuous, distributed tracing. Sentry Performance, Scout APM, New Relic, and Datadog instrument every transaction, break it down by DB, cache, and external calls, and alert on regressions. The key is sampling: set a low traces_sample_rate so overhead stays negligible.

Sentry is a common choice because it ties errors and performance together. A minimal Django setup:

# settings.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn="https://<your-key>@o0.ingest.sentry.io/0",
    integrations=[DjangoIntegration()],
    # Sample a small fraction of requests for performance tracing in prod.
    traces_sample_rate=0.1,
    send_default_pii=False,
)

Sentry's performance view even auto-detects N+1 queries and slow spans across services. To wire Sentry into Django end to end, see our walkthroughs on monitoring a Django application's live events with Sentry and setting up Sentry as an event-tracking platform.

What about the original django-web-profiler package?

The django-web-profiler package (the subject of the original version of this article) is a Django request/SQL logger: it records per-URL statistics — device, IP, user and system CPU time, query counts, SQL time, and cache calls — to log files, and exposes a basic UI plus a logging_urls management command for capturing stats with DEBUG = False.

It still installs, but it is not actively maintained, so we no longer recommend it for new projects. If you only need request/SQL logging, the combination of django-silk (persisted UI) plus structured logging covers the same ground with active support. For historical reference, here is the original setup:

# Legacy / unmaintained — shown for reference only.
pip install django-web-profiler

# settings.py
INSTALLED_APPS = [
    # ...
    "debug_toolbar",
    "django_web_profiler",
]

MIDDLEWARE = [
    # ...
    "django_web_profiler.middleware.DebugLoggingMiddleware",
]

INTERNAL_IPS = ("127.0.0.1",)

# Capture URL statistics with DEBUG=False:
#   python manage.py logging_urls

A practical profiling workflow

  1. Reproduce the slow request and open it with django-debug-toolbar; check the SQL panel first.
  2. If query count is high or duplicated, fix the N+1 with select_related/prefetch_related and re-measure.
  3. If queries are few but slow, run EXPLAIN ANALYZE and add indexes or rewrite the query.
  4. If the DB is fine but the request is still slow, profile the Python with cProfile/line_profiler.
  5. In production, lean on py-spy for ad-hoc investigation and an APM for continuous tracing.

Measure, change one thing, measure again. That loop is the whole discipline.

Frequently Asked Questions

What is the best Django profiling tool in 2026?

There is no single best tool — you layer them. django-debug-toolbar is the standard for development because its SQL panel instantly reveals query counts and N+1 problems. django-silk is best when you need to persist results or profile staging. In production, a sampled APM (Sentry Performance, Scout, New Relic, or Datadog) plus py-spy for ad-hoc work is the right combination.

Can I run django-debug-toolbar in production?

No. The debug toolbar is development-only: it renders only when DEBUG = True and the client IP is in INTERNAL_IPS, it exposes internal data such as SQL and settings, and it adds significant per-request overhead. For production use a sampling profiler (py-spy) or an APM with a low traces_sample_rate.

How do I find N+1 queries in Django?

Open a request in debug-toolbar's SQL panel (or django-silk) and look for the same query repeated once per row. That is the N+1 signature. Fix it with select_related() for forward ForeignKey/OneToOne relations and prefetch_related() for reverse FK and many-to-many relations. Our Django database access optimization guide covers the full pattern.

How do I profile only the database queries Django runs?

With DEBUG = True, inspect django.db.connection.queries for the SQL and timing of the current request, or call str(queryset.query) to see the SQL a queryset would generate without running it. Use QuerySet.explain(analyze=True) for the database's execution plan, and enable the database slow-query log to catch slow statements in production.

Is django-web-profiler still maintained?

No. django-web-profiler still installs and logs per-URL request/SQL statistics, but it is no longer actively maintained, so it is not recommended for new projects. Use django-silk for a persisted profiling UI and the built-in connection.queries/EXPLAIN ANALYZE for raw SQL inspection instead.

How do I profile a Django app in production without slowing it down?

Use low-overhead, sampled tooling. py-spy attaches to a running process by PID and samples the call stack with no code change, so it is safe on live services. An APM such as Sentry Performance keeps overhead negligible by tracing only a fraction of requests via traces_sample_rate. Never enable debug-toolbar or full cProfile on every production request.

Need help making your Django app fast?

MicroPyramid has been building and tuning Django applications since 201412+ years and 50+ projects of profiling, query optimization, and scaling work. If your app is slow under load and you want a concrete optimization plan, explore our Django development services or contact us to talk it through.

Share this article