`bulk_create()`, `bulk_update()`, and what they skip
coreadvancedCalling `save()` in a loop over 50,000 objects issues 50,000 statements. `bulk_create()` and `bulk_update()` collapse that into a handful, and `batch_size` controls how many rows go in each one. The price is that they are deliberately dumb: Django's documentation states that "the model's `save()` method will not be called, and the `pre_save` and `post_save` signals will not be sent". Anything your `save()` override does — stamping a field, updating a search vector, writing an audit row — silently does not happen. That is a feature when you want raw speed and a serious bug when you did not know about it.
Think of it as
Think of these as writing rows rather than saving objects. Every convenience the ORM normally layers on top of a write lives in `Model.save()` and in the signals it fires, and both are bypassed here by design — that is where the speed comes from. So the question before using them is always "what else was happening when this object saved?", and the honest way to answer it is to read the model's `save()` and grep for receivers on its signals, rather than to assume. Where the behaviour is genuinely needed, the fix is not to abandon bulk writes but to do the same work in bulk too: compute the derived fields in the loop that builds the objects, and write the audit rows with their own `bulk_create`. `batch_size` is the second half. It bounds how many rows go into one statement, and it matters for two different reasons: a very large statement can exceed what the database or driver will accept, and for `bulk_update` the generated SQL grows with the number of objects because each one contributes its own `WHEN` clause. Django's docs also warn that `bulk_update()` "prepares all of the `WHEN` clauses for every object across all batches before executing any queries" — so the SQL text for the entire operation is built up front, and a very large list is a memory cost even with a small batch size. The remaining rule is about identity: on PostgreSQL `bulk_create` can return primary keys, but not when `ignore_conflicts` is on, and `bulk_update` cannot change a primary key at all.
What we're doing: Import 200,000 rows safely: chunked, re-runnable, and with the derived values the skipped `save()` would have set.
- 11–12
- The two fields the model's `save()` normally derives, computed here instead. This is the honest way to use a bulk write: replace the skipped behaviour rather than lose it.
- 18–21
- The upsert. `unique_fields` must match a real unique constraint, and leaving `created_at` out of `update_fields` is what stops a re-run rewriting the original import time.
- 25–29
- The audit rows the `post_save` receiver would have written, done as one more bulk insert. One extra statement per chunk instead of one per row.
- 37–38
- Reading with `.iterator()` and writing with `bulk_update()` — the read side and the write side each need their own bound, and neither one fixes the other.
- 41–43
- Flushing at 1,000 keeps both the accumulator and the generated SQL bounded. Django builds all the `WHEN` clauses up front, so a single call with 200,000 objects is a large memory cost regardless of `batch_size`.
Why this works: The import is bounded on input, safe to re-run, and reproduces both pieces of behaviour the bulk path skips — which is what makes it a replacement for the loop rather than a faster way to lose data quality.
Assuming `bulk_create` runs your `save()` override
Wrong
Better
What you see: The import reports success and a fifth of the site 404s, because every imported record has an empty slug. The rows are present and look fine in the admin, so the cause is not obvious for some time.
Why: Django states the caveat plainly: `save()` is not called and `pre_save`/`post_save` are not sent. Everything an application layers onto saving — derived fields, denormalised counters, search vectors, audit trails, cache invalidation — lives in exactly those two places, so a bulk write silently skips all of it. The rule is to read the model's `save()` and check for signal receivers before switching, then reproduce whatever they did in the loop that builds the objects.
- Whole: Order.objects.bulk_create(new_orders, batch_size=500, update_conflicts=True, update_fields=["total"], unique_fields=["reference"])
- bulk_create — skips save() and signals: The documented caveat: "the model's save() method will not be called, and the pre_save and post_save signals will not be sent." Read the model's save() before choosing this.
- new_orders — unsaved instances, built in memory: The list itself is a memory cost. For very large inputs, build and write it in chunks rather than assembling millions of objects first.
- batch_size=500 — rows per statement: Bounds statement size so the database or driver does not reject it. Without it Django uses one statement for everything, which can be very large.
- update_conflicts=True — upsert instead of error: A conflicting row is updated rather than raising `IntegrityError`. This is what makes a re-run of an import safe.
- update_fields=["total"] — what a conflict overwrites: Only these columns are updated on conflict. Omitting a field means an existing row keeps its current value for it — which is usually what you want for `created_at`.
- unique_fields=["reference"] — what counts as a conflict: Must match a real unique constraint on the table. This is the column the database uses to decide "the same row", so it defines what a re-run means.
Which write to reach for
Together
Remember: Bulk writes write rows; they do not save objects. `save()`, `pre_save` and `post_save` are all skipped, which is where the speed comes from and where the data-quality bugs come from — so read the model's `save()` and its signal receivers first, then reproduce that work in the loop that builds the objects, in bulk. Use `update_conflicts` with `unique_fields` to make an import re-runnable, and keep `created_at` out of `update_fields`. Bound both sides: `.iterator()` on the read, a flush counter on the write, because `bulk_update` builds every `WHEN` clause before it executes anything.
See also: database side updates and batched deletes · iterator batching and streaming responses · side effects and when to avoid signals

