skip to content

A Django checkout page takes three seconds locally; how do you use django-debug-toolbar's panels to find where the time goes?

level: middleimportance: must knowfreq 56%

answer

  1. start with elapsed vs CPU
  2. count, similar, duplicated
  3. stack trace to the line
  4. templates, cache, signals next
  5. profiling panel is off by default

basics

~20 s

Read the Timer panel to see whether time is CPU or waiting, then the SQL panel's count, slow queries and similar or duplicated groups, whose stack traces point at the triggering line. Templates, Cache and Signals panels explain the rest.

solid answer

~50 s

I start with the **Timer** panel: if elapsed time is far above CPU time, the request is waiting, usually on the database. The **SQL** panel then gives the query count and total time, highlights queries slower than `SQL_WARNING_THRESHOLD` (500 ms by default), and groups repeats as **similar** (same SQL, different parameters) or **duplicated** (same SQL and parameters). Each query has a stack trace to the Python or template line that ran it, and buttons to re-run a `SELECT` or see its `EXPLAIN`. The **Templates** panel shows what rendered with which context, **Cache** shows calls and hit or miss counts, and **Signals** lists receivers that may fire on save. If it is CPU-bound, I enable the **Profiling** panel, which ships disabled. The fix, such as `select_related`, is ORM tuning; the toolbar's job is to point at the line.

code

python · 7 lines
python
# settings.py (development)
DEBUG_TOOLBAR_CONFIG = {
    # Highlight queries slower than 100 ms instead of the 500 ms default.
    "SQL_WARNING_THRESHOLD": 100,
    # Leave only the Redirects panel disabled, so Profiling is on.
    "DISABLE_PANELS": {"debug_toolbar.panels.redirects.RedirectsPanel"},
}

go deeper

for a junior

Know that the SQL panel shows every query with its time and that a large count on one page is a warning sign.

for a middle

Walk from Timer to SQL, read similar versus duplicated groups, and use stack traces to find the triggering template or Python line.

for a senior

Separate waiting from computing, remember EXPLAIN ANALYZE executes, and treat development timings as relative, confirming with production-like data.

for a principal

Turn local findings into guardrails, such as query budgets checked in tests, so the next slow page is caught before it ships.

## The scenario A checkout page renders the cart, shipping options and a summary, and it takes three seconds on a laptop with a small database. Guessing is slow; django-debug-toolbar shows what one request actually did. The panels below are all in the toolbar's default panel list, and each answers a different question. ## Step 1: Timer, to classify the problem The **Timer** panel reports user and system CPU time, total CPU time, elapsed time and, in current releases, how much of the elapsed time was the toolbar itself. The comparison that matters: - **elapsed much larger than CPU**: the process was waiting, on the database, the network or a sleep, so go to the SQL panel; - **CPU close to elapsed**: Python is busy, so go to profiling. ## Step 2: SQL, to find what the database did The **SQL** panel lists every query the request ran, per database alias, with its time. What to read, in order: 1. **The count and total time** at the top. Tens of queries on a checkout is common; hundreds is a pattern. 2. **Similar** and **duplicated** markers. The toolbar groups queries whose SQL text is the same as *similar*, and those whose SQL **and parameters** are the same as *duplicated*, and colour-codes each group. One similar group of 120 queries, one per cart line, is a loop issuing a query per object. A duplicated group means the same query ran again with the same values, which caching or reuse would remove. 3. **Slow queries**, highlighted when above `SQL_WARNING_THRESHOLD` (500 ms by default). 4. **Stack traces**, enabled by `ENABLE_STACKTRACES` (default `True`). They name the file and line, including template lines, which is how you find the `{% for line in cart.lines.all %}` that runs the query. 5. **Sel** and **Expl** buttons. *Sel* re-runs a `SELECT` and shows the rows; *Expl* shows the plan. On PostgreSQL the toolbar runs `EXPLAIN ANALYZE`, which executes the statement. ## Step 3: the supporting panels | Panel | What it tells you on this page | |---|---| | **Templates** | which templates and includes rendered, and their context (switch context off with `SHOW_TEMPLATE_CONTEXT` if it is huge) | | **Cache** | each cache call with hits and misses; its docs note hit/miss counts may not always be accurate, and the panel does not work with per-site caching | | **Signals** | the signals defined and the receivers connected to each, useful when a `post_save` receiver does work on every saved order line | | **Request** / **Headers** | the view that handled the request, session data, cookies and headers | | **History** | earlier requests, including fetch calls made by the checkout's JavaScript | ## Step 4: Profiling, when it is CPU The **Profiling** panel runs the request under Python's profiler and shows a call tree. It is included but **disabled by default** (it and the Redirects panel are in the `DISABLE_PANELS` option). Enable it by removing it from that set in `DEBUG_TOOLBAR_CONFIG`. On Python 3.12 and later the toolbar's docs say to run `runserver --nothreading`, because concurrent requests do not work with it. ## What the toolbar does not do - It does not fix anything. Turning a similar group into one query is the job of the ORM's query-tuning tools. - It measures one request in development, with development data. A page that runs 40 fast queries on 20 cart lines may behave very differently with 2,000 rows. - It adds overhead of its own, especially with stack traces and template context capture on, so read times relatively rather than as production numbers. ## A worked reading Suppose the SQL panel reports 184 queries in 2.6 seconds, with one similar group of 150 whose stack traces all point at a line in `checkout/summary.html` that reads each line's product, and one duplicated group of 4 from a method that loads the shipping zones. That reading already contains the plan: batch the per-line product loads, and compute the shipping zones once per request. After the change, reload the page and compare the count and time in the same panels; the toolbar is as useful for confirming a fix as for finding the problem.

  • In django-debug-toolbar, why can clicking Expl on a query be risky with PostgreSQL?
    For PostgreSQL the toolbar runs `EXPLAIN ANALYZE`, which really executes the statement to measure it. On a development database that is usually harmless for a `SELECT`, but it still costs the full query time, and pointing the toolbar at a shared database means your clicks run real queries there.
  • The django-debug-toolbar SQL panel shows the same query duplicated four times with identical parameters; what does that suggest?
    Something computes the same data several times in one request, for example a template calling a method that queries, used in four places, or a property with no caching. Similar-but-not-duplicated groups point at per-object loops; exact duplicates point at recomputation, fixed by evaluating once and reusing the result.

saying these in an interview costs you the question

  • Duplicated queries in the toolbar means same SQL with different parameters
  • The Profiling panel is active by default
  • Toolbar timings are what production users will experience
  • The Timer panel's elapsed time is pure database time
  • The toolbar rewrites slow queries automatically