Odoo Development
Could Not Serialize Access Due to Concurrent Update
This error is PostgreSQL refusing to let two transactions commit incompatible changes to the same row. Odoo expects it, catches it and retries five times before you ever see it, which is why the message reaching a user means something more specific than a collision.
Part of our guide to Odoo Development
What the error actually is
The message comes from PostgreSQL, not from Odoo:
ERROR: could not serialize access due to concurrent update
Odoo sets every cursor to the REPEATABLE READ isolation level. Under that level, a transaction sees a consistent snapshot of the database as it existed when the transaction began. If your transaction then tries to update a row that a different transaction has modified and committed since that snapshot was taken, PostgreSQL has no correct way to reconcile the two, so it aborts the later one.
The Odoo source is explicit about this choice. The comment in odoo/sql_db.py explains that Odoo relies on snapshot isolation and implements its own locking for the cases most likely to collide, such as stock reservations and sequence updates, rather than paying the performance cost of full SERIALIZABLE.
Two things follow. Nothing is partially written, because the whole transaction rolls back. And the error is expected behaviour under contention rather than a sign of a damaged database.
Odoo already retried five times
This is the part most explanations of this error miss, and it changes the diagnosis completely.
Request handling is wrapped in a retrying() function in odoo/service/model.py. It catches three PostgreSQL error classes, SERIALIZATION_FAILURE, LOCK_NOT_AVAILABLE and DEADLOCK_DETECTED, and retries the whole call. The behaviour is identical in Odoo 17, 18 and 19:
- The maximum number of attempts is five, defined as
MAX_TRIES_ON_CONCURRENCY_FAILURE - Between attempts it rolls back the cursor, resets the transaction and the registry changes, and reloads the session
- It sleeps for
random.uniform(0.0, 2 ** attempt)seconds, so a burst of colliding requests spreads out instead of retrying in lockstep - It rewinds any uploaded files so the retry reads them from the beginning
- On the final failure it logs
maximum number of tries reached!and re-raises
So the ordinary case, two users saving the same record at the same moment, never reaches anyone. One of them retries and succeeds a fraction of a second later.
If a user saw this error, the retry budget was exhausted. That is a much stronger signal than a collision. It means the same row was contended across five attempts spread over several seconds, which means there is a hotspot rather than bad luck.
The four hotspots, in the order worth checking
1. A "No gap" sequence
This is the most common cause in production and the easiest to fix.
Odoo sequences have two implementations, Standard and No gap. The Standard implementation uses a PostgreSQL sequence, which is designed for concurrency and does not block. The No gap implementation cannot, because guaranteeing unbroken numbering means nobody else may take a number until the current transaction finishes.
To do that, _update_nogap in ir_sequence.py runs:
SELECT number_next FROM ir_sequence WHERE id=%s FOR UPDATE NOWAIT
NOWAIT means the second transaction does not queue, it fails immediately with LOCK_NOT_AVAILABLE. Every document that draws from that sequence now serialises through one row.
At low volume this is invisible. At a few invoices a second, or during an import, it is a wall. If a regulator genuinely requires gapless numbering, keep it and reduce the concurrency around it. If nobody requires it, and frequently nobody does, switch that sequence to Standard.
2. A hot parent record
Some records are written by many transactions at once by their nature. A product whose stock everyone is reserving. A single order accumulating lines from a fast process. A partner being updated by an integration while a user edits it.
The fix is usually to stop writing to the parent. A stored computed field on a parent that depends on its children means every child write becomes a parent write, and every parent write is a collision candidate. Ask whether that field really needs to be stored, and whether a related field would do the same job without the write.
The same applies to constraints that read across siblings: a Python constraint on a child that re-reads the whole parent turns one write into a wide read on every save.
3. A cron job overlapping live users
Scheduled jobs run against the same data users are working in. A job that touches many records for a long time is holding a large collision surface.
Odoo's cron code acknowledges this directly. The comment on the job acquisition query notes that it may raise a serialisation failure when another worker has taken the job, and that the correct response is to roll back and move on to the next one.
Move heavy jobs outside working hours, keep their transactions short, and process in batches that commit rather than in one long sweep. Scheduled actions covers how to structure this.
4. Custom code writing inside a loop
The classic shape:
for line in order.order_line:
line.order_id.write({'some_total': ...})Every iteration writes the same parent row, in the same transaction, and multiplies the window in which another process can collide. Compute once and write once to the whole recordset.
This is the same discipline that keeps ORM code fast in general, covered in coding standards and computed fields.
How to diagnose it properly
1. Search the log for the retry lines. Odoo logs each retry with the error name and how many tries are left, then logs maximum number of tries reached! when it gives up. If you see retries but no exhaustion, the system is absorbing normal contention and there is nothing to fix.
2. Find the failing statement. The traceback contains the SQL. The table name in it is the hotspot, and that alone usually identifies which of the four causes you have.
3. Check whether it correlates with a cron. Compare the timestamps against your scheduled actions. Overlap is the answer more often than expected.
4. Check your sequences. Look at which sequences use the No gap implementation and whether any of them sit on a high-volume document.
5. Only then look at the code. By this point you know the table, the timing and the concurrency source, so the review is targeted.
What not to do
- Do not raise the retry count. Five attempts with exponential backoff already covers incidental collision. If that was not enough, more attempts hold connections longer and make the contention worse.
- Do not catch and ignore it. Swallowing the exception does not complete the transaction, it just stops anyone finding out that the work did not happen.
- Do not switch the isolation level. Odoo's ORM, its caching and its locking assume
REPEATABLE READ. Changing it does not remove contention, it changes which failures you get. - Do not treat it as an infrastructure problem. Bigger hardware processes the same collision faster and more often.
If the contention appeared after an upgrade rather than after a code change, check what moved. A stored compute or a constraint that was cheap on the old version can behave differently once field definitions change, which is the kind of thing what actually breaks in an upgrade and migration scripts both touch on.
If this is happening on a live system and the hotspot is not obvious from the log, it is worth an Odoo health check. Contention of this kind is usually one specific piece of configuration or one loop, and it is far quicker to find with someone reading the log alongside the code than by changing things and waiting.
FAQ
Questions, answered.
What does could not serialize access due to concurrent update mean in Odoo?
It means two transactions tried to change the same database row, and PostgreSQL cancelled the second one. Odoo runs every cursor at the REPEATABLE READ isolation level, where each transaction sees a snapshot of the database taken when it started. If your transaction updates a row that another transaction has already modified and committed since that snapshot, PostgreSQL cannot merge them, so it aborts yours.
This is normal database behaviour, not corruption and not a bug. Nothing is half-written, because the whole transaction rolls back.
What matters is that Odoo already handles it. The retrying() wrapper in odoo/service/model.py catches serialisation failures, rolls back, waits, and calls the request again up to five times. So an ordinary two-users-clicked-at-once collision is invisible.
Expert tip. If a user actually sees this message, the retries were exhausted. That is not a random collision, it is sustained contention on one row, and the fix is to find the hotspot rather than to retry harder.
How do I fix the concurrent update error in Odoo?
Find which row is contended, then remove the contention. Retrying harder does not help, because Odoo has already retried five times before the error surfaced.
Work through the four common hotspots in order:
1. A "No gap" sequence. Odoo's no-gap implementation takes SELECT ... FOR UPDATE NOWAIT on the sequence row, so every document creation serialises on it. Under load this is the single most common cause. Switch to the Standard implementation unless a regulator genuinely requires unbroken numbering.
2. A hot parent record. Stock reservations against one product, or many lines writing to one order, funnel every write onto the same row.
3. A cron overlapping a user. A scheduled job holding rows while users work on them collides by design.
4. Custom code writing in a loop. Per-record writes inside a loop multiply the collision window. Batch the recordset and write once.
Common mistakes
- Raising the retry count instead of removing the hotspot
- Adding a
try/exceptthat swallows the error, which hides data loss rather than preventing it - Assuming it is a hardware or connection problem
Does Odoo retry the concurrent update error automatically?
Yes. Odoo's retrying() function wraps request handling and retries on three PostgreSQL error classes: serialisation failure, lock not available, and deadlock detected.
The behaviour is specific and identical in Odoo 17, 18 and 19:
- Up to five attempts in total, set by
MAX_TRIES_ON_CONCURRENCY_FAILURE - Between attempts the transaction is rolled back, the transaction and registry changes are reset, and the session is reloaded
- It waits a random interval with exponential backoff,
random.uniform(0.0, 2 ** attempt), so retries spread out rather than colliding again immediately - Uploaded files on the request are rewound so the retry sees them from the start
- When the last attempt fails it logs "maximum number of tries reached" and re-raises
That log line is the one to search for. It tells you the retry budget was spent, which distinguishes real contention from noise.
Expert tip. If a request uploads a non-seekable file, Odoo cannot rewind it and raises "Cannot retry request on input file" instead of retrying. A file upload endpoint that fails under concurrency is usually this, not the underlying collision.
Why does this error happen during an Odoo module update or on Odoo.sh?
Because a module update rewrites shared configuration rows while something else is still touching them. The usual something else is a cron job, a second worker, or a website request loading assets.
During an update Odoo writes to ir_module_module, view definitions, and asset bundles. Those are exactly the rows every other process reads and occasionally writes. On a multi-worker deployment or a build platform, two processes can easily meet there.
Practical measures:
- Stop or pause cron jobs during the update, which removes the most common competitor
- Update with a single worker where the deployment allows it
- Avoid triggering an update while a build or a second update is already running
Expert tip. If the failing statement involves asset bundles, the collision is usually two workers regenerating assets at once after a restart. Warm the assets with a single request before opening the instance to traffic and it stops happening.
Keep reading
More on Odoo Development.
The full guide, plus the other articles in this cluster.
With CODEerts
Need something built properly?
We are certified Odoo partners. We build custom Odoo modules that extend the framework rather than fight it, and we publish our own apps on the Odoo Store.
See how we can helpBook a callReady to make Odoo work the way your business does?
Book a free callCODEerts is a team of certified Odoo partners and full-stack engineers. We implement, customise and support Odoo ERP, then build the software around it.