See what django-mindoff actually does, then measure it against the tools you already reach for.
This is a small, runnable Django project that puts django-mindoff to work on real endpoints. It is not a tutorial app and it is not a starter template. Its job is to let you read a few files, call a few endpoints, and decide for yourself whether the framework can be of use in your stack.
For bulk work over a Django model (create, read, update), it runs the same job four ways: a DRF serializer with many=True, pandas, plain Polars, and django-mindoff. Every number is measured the same way and written out as a CSV plus charts, so you are never asked to trust a claim you cannot reproduce.
Framework: https://github.com/mindoffwork/django-mindoff
Documentation: https://django.mindoff.work
Package: https://pypi.org/project/django-mindoff/
The charts below are the actual output of the latest run, committed to benchmarks/ and re-rendered every time the benchmark runs, so what you see here is whatever the last run measured.
| Create | Read | Update |
|---|---|---|
![]() |
![]() |
![]() |
Each chart plots time and memory as the row count grows, for every approach that has a real workflow for that operation. If you want the raw figures behind the lines, every measured value, across every row count we ran, sits in benchmarks/catalog_benchmark_values.csv. It includes a remarks column that explains each comparison we left out and why.
The shape of the result is steady across all of it. For bulk create, read, and update, django-mindoff finishes faster and uses less memory than the DRF serializer with many=True, pandas, and plain Polars, and the lead grows as the data gets bigger. The eager lane does full, model-aware validation, the same work a serializer does, just vectorized instead of row by row. The streaming lane skips validation to stay flat on memory, because it never loads the whole result set at once.
Worth saying plainly: at small batch sizes, plain Django usually wins. django-mindoff with polars pays a fixed cost to build frames and run vectorized validation, and below a few thousand rows that overhead does not pay off. This is a complement for bulk tabular work, not a replacement for the ORM, and the benchmark is built to show both sides of that. When you run benchmark yourself, you can reduce the sample size to see where ORM wins to understand better when to use which.
Keeping this honest is cheap. When a new django-mindoff release lands, we bump the pin in requirements.txt, run the benchmark, and drop the fresh files into benchmarks/. This README hardcodes no figures and no version, so it never needs editing for a new run. The exact version any result was produced with is always the one pinned in requirements.txt.
Every feature maps to a file you can open. Reading the code is the point, so the README does not repeat what the docstrings already explain.
| To see this | Open this | Endpoint |
|---|---|---|
| A managed API and the standard response envelope | apps/shop/apis/get_profile.py | GET /v1/shop/get_profile/ |
| Payload validation and versioning on one route | apps/shop/apis/create_order.py | POST /v1/shop/create_order/, POST /v2/shop/create_order/ |
| Bulk row validation that keeps good rows and rejects bad ones | apps/catalog/apis/import_products.py | POST /v1/catalog/import_products/ |
| A queryset turned into a Polars frame and aggregated without a loop | apps/catalog/apis/list_products_report.py | GET /v1/catalog/list_products_report/ |
| The create, read, and update benchmark | apps/catalog/components/products.py | POST /v1/catalog/run_product_benchmark/ |
| A queue-mode API driven by one config switch | apps/jobs/apis/generate_inventory_report.py | POST /v1/jobs/generate_inventory_report/ |
A good place to start reading is a components/ file, for example apps/catalog/components/products.py, which walks through the whole catalog flow.
You need Python 3.12 or 3.13.
git clone https://github.com/mindoffwork/django-mindoff-benchmark.git
cd django-mindoff-benchmarkpython -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activatepip install -r requirements.txtThis pulls django-mindoff[internal], which brings DRF, Polars, ConnectorX, and the rest of the runtime stack, plus the two chart helpers this repo needs.
cp .env.example .envSettings read a secret key and a REDIS_URL through this file, and the server will not start without it. The defaults in .env.example are fine for local use.
python manage.py migrate
python manage.py runserverOpen http://127.0.0.1:8000/ for the landing page, then try the endpoints below.
A note on
DEBUG. Settings read it from.env. If your shell already exports a globalDEBUGvariable, python-decouple may pick that one up instead, so keep it boolean-friendly:$env:DEBUG="True"
The easiest way in is Postman. Import django-mindoff-showcase.postman_collection.json and you get every request below, plus a base_url variable already set to http://127.0.0.1:8000. If you would rather stay in the terminal, here is the same thing with curl.
curl http://127.0.0.1:8000/v1/shop/get_profile/
curl -X POST http://127.0.0.1:8000/v1/shop/create_order/ \
-H "Content-Type: application/json" \
-d '{"customer_name":"Asha","customer_email":"asha@example.com","product_name":"Wireless Mouse","quantity":2}'
# same route, richer v2 response
curl -X POST http://127.0.0.1:8000/v2/shop/create_order/ \
-H "Content-Type: application/json" \
-d '{"customer_name":"Asha","customer_email":"asha@example.com","product_name":"Mechanical Keyboard","quantity":1}'# one valid row, one bad row (empty sku, negative price) that gets rejected
curl -X POST http://127.0.0.1:8000/v1/catalog/import_products/ \
-H "Content-Type: application/json" \
-d '{"rows":[{"sku":"SKU-001","name":"Keyboard","price":49.99,"stock":20,"category":"accessories"},{"sku":"","name":"Bad Product","price":-10,"stock":5,"category":"misc"}]}'
curl http://127.0.0.1:8000/v1/catalog/list_products_report/Only this endpoint needs Redis and Dramatiq. Skip it and everything else still works.
redis-server
dramatiq django_mindoff.queue_worker
curl -X POST http://127.0.0.1:8000/v1/jobs/generate_inventory_report/ \
-H "Content-Type: application/json" \
-d '{"report_name":"weekly_inventory_snapshot"}'This is the part written for a skeptical reader. The whole thing lives in apps/catalog/components/, where config.py holds the methodology as plain constants and measure.py holds the measurement code.
curl -X POST http://127.0.0.1:8000/v1/catalog/run_product_benchmark/ \
-H "Content-Type: application/json" \
-d '{"row_count":[10000,50000,100000],"iterations":5}'That writes four files into output/:
catalog_benchmark_values.csv, every measured number, with aremarkscolumn that records each comparison we deliberately skipped and why.catalog_benchmark_create.png,catalog_benchmark_read.png, andcatalog_benchmark_update.png, one file per operation, each with a time panel and a memory panel.
The published copies in benchmarks/ came from exactly this command, run against the django-mindoff version pinned in requirements.txt.
Each operation is judged on both time and memory against every method that has a real workflow for it. A method with no idiomatic path for an operation is skipped, and the skip is called out with a footnote on the chart and a row in the CSV.
- Create compares the DRF serializer, pandas, and django-mindoff.
- Read is queryset to frame to CSV, so it compares pandas (
from_records), plain Polars (pl.DataFrame(list(qs))), and django-mindoff in both its eager and streaming modes. DRF sits out, because serializers build API representations, not dataframes. - Update compares the DRF serializer and django-mindoff. pandas sits out, because it has no idiomatic bulk-update path.
- Realistic input. Bulk rows come from a Parquet file, so each engine reads data the way it actually would, rather than from hand-built Python objects that would unfairly tax some engines with construction overhead.
- Time is the median of at least five measured runs after one warmup.
- Memory is peak RSS delta, sampled in a separate worker process for each lane, so native Polars and Arrow allocations are counted without noise from the long-running server.
- Validation is labeled per line. The eager django-mindoff and DRF create and update paths run full model-aware validation. The streaming django-mindoff and pandas paths skip it. The chart legend and the CSV say which is which, so a validated path is never quietly compared against an unvalidated one.
- Query count is a diagnostic, not a score. django-mindoff bulk writes can persist through a path that Django's cursor instrumentation never sees, so a
0there does not mean no database work happened. The output says as much. - Read results are checked for parity across engines before any timing counts, so everyone is producing the same rows.
The committed charts run on local SQLite, which is the worst case for django-mindoff. SQLite is single-writer and has no native bulk-load path. For numbers closer to production, point the project at PostgreSQL or MySQL and turn the row counts up:
{ "row_count": [50000, 250000, 1000000], "iterations": 5 }A file lock plus SQLite WAL and busy_timeout settings keep repeated local runs from colliding on the shared demo database.
pytestA couple of focused examples:
pytest apps/shop/tests/test_apis/test_create_order.py -q
pytest apps/catalog/tests/test_apis/test_run_product_benchmark.py -qCI runs the same suite on Python 3.12 and 3.13 for every pull request and every push to the protected branch. The workflow is in .github/workflows/ci.yml.
apps/
shop/ managed APIs, response envelopes, payload validation, v1 and v2 versioning
catalog/ bulk import, Polars reporting, and the create/read/update benchmark
jobs/ the optional queue-mode API (Redis and Dramatiq)
config/ settings, root URLs, and the response-code registry (responses.csv)
benchmarks/ the published benchmark artifacts (charts and CSV), tracked in git
mindoff.py the framework's project manager for scaffolding commands
Each app follows the same shape: thin handlers in apis/, reusable logic in components/, and tests in tests/.
Contributions are welcome. CONTRIBUTING.md covers setup, the commit style, and how to keep a benchmark change honest. By taking part you agree to the Code of Conduct. If you find a security issue, please read the Security Policy first and report it privately. Past changes are recorded in the Changelog.
This project uses the BSD 3-Clause License, the same one django-mindoff uses. See the LICENSE file for the full terms.


