Skip to main content

drf-nplus

A DRF serializer-aware N+1 query detector for Django.

Unlike a generic query counter, drf-nplus attributes each SQL query to the serializer field that triggered it and suggests the ORM fix:

[drf-nplus] GET /posts/ → 201 queries in 340.2ms | 2 repeated SQL templates (possible N+1)
  PostSerializer.author: 100 queries  ← possible N+1 — add .select_related("author")
  PostSerializer.tags: 100 queries  ← possible N+1 — add .prefetch_related("tags")
  PostSerializer: 1 queries

Install

pip install drf-nplus

Or from source:

pip install -e /path/to/drf_nplus_project

Integrate

Add the middleware to your Django settings.py:

MIDDLEWARE = [
    # ...
    "drf_nplus.QueryCountMiddleware",
]

That's it. On every request the middleware logs a per-field query report and sets these response headers:

  • X-DRF-Queries — total query count
  • X-DRF-Query-Time-Ms — wall time spent in get_response
  • X-DRF-NPlus-Fields — comma-joined serializer field paths flagged as N+1

Configure

All settings are optional. Defaults shown:

DRF_NPLUS = {
    "ENABLED": True,             # gate the middleware; typically set to DEBUG
    "LOGGER": "drf_nplus",       # standard `logging` logger name
    "LOG_LEVEL": "WARNING",      # level used to emit the report
    "THRESHOLD": 2,              # ≥ N identical queries from one field = N+1
    "IGNORE_PATHS": (),          # prefixes to skip, e.g. ("/admin/", "/static/")
    "RESPONSE_HEADERS": True,    # set the X-DRF-* headers
}

Wire the logger through your LOGGING config to route reports wherever you already send logs:

LOGGING = {
    "version": 1,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {
        "drf_nplus": {"handlers": ["console"], "level": "WARNING"},
    },
}

Use in tests

Installing drf-nplus auto-registers a pytest plugin. Two ways to guard tests:

Per-test marker:

import pytest

@pytest.mark.no_nplus
def test_post_list_is_efficient(db):
    client.get("/posts/")

# Or with a custom threshold
@pytest.mark.no_nplus(threshold=5)
def test_moderate(db): ...

Suite-wide: pytest --nplus-strict (applies the guard to every test).

Manual context manager (for finer control):

from drf_nplus.testing import assert_no_nplus
from blog.serializers import PostSerializer
from blog.models import Post

def test_post_list_is_efficient(db):
    qs = Post.objects.select_related("author").prefetch_related("tags")
    with assert_no_nplus():
        PostSerializer(qs, many=True).data

All three raise drf_nplus.NPlusOneDetected (an AssertionError subclass) with the offending field paths, SQL, and suggested fix.

How it works

  • patches.install() wraps rest_framework.serializers.Serializer.to_representation to push the current field name onto a ContextVar stack for the duration of get_attribute / to_representation.
  • The middleware attaches an execute_wrapper to every configured database connection. When a query fires, the current stack path is snapshotted and attributed to that field.
  • ContextVar (not threading.local) so it behaves correctly under async views.

Example project

This repo ships a blog/ app that demonstrates the problem:

python manage.py migrate
python manage.py shell < seed.py
python manage.py runserver

# Unoptimized — ~201 queries
curl -s http://127.0.0.1:8000/posts/ > /dev/null
# Optimized — ~3 queries
curl -s http://127.0.0.1:8000/posts-optimized/ > /dev/null

Development

pip install -e ".[dev]"
pytest

Release files for drf-nplus 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for drf-nplus 0.1.0
File Size Uploaded
drf_nplus-0.1.0.tar.gz 11.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for drf-nplus 0.1.0
File Interpreter ABI Platform
drf_nplus-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 23.0 kB

Release files / drf_nplus-0.1.0.tar.gz

Download URL drf_nplus-0.1.0.tar.gz
Size 11.6 kB
Tags Source
SHA-256 checksum
How to use checksums
6697e241b5c737ebfbbe4a12c4a8bc3c7e9ced4e7ad75e220eabbd6254cee806
BLAKE2b-256 checksum
How to use checksums
9c0f5a3e14e9d8d37e61592877602d975e140aec476b7d1327984ad6870e494c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release files / drf_nplus-0.1.0-py3-none-any.whl

Download URL drf_nplus-0.1.0-py3-none-any.whl
Size 11.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6ced643ce0f0e31a8a55e2218e7c5a59555c61d8cffc37599508dfd6741c313b
BLAKE2b-256 checksum
How to use checksums
9b8567bddb659addf73363a3fad76e6f83441d00a0436dd55b2014a47558249b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page