How to Understand the Concurrent Update Issue in PostgreSQL When Using Odoo

PostgreSQL is well known for its reliability and strong transaction consistency. However, developers working with applications such as Odoo often encounter an error similar to:

ERROR: could not serialize access due to concurrent update

At first glance, this error may appear to be a database problem. In reality, PostgreSQL intentionally prevents data corruption and maintains consistency between concurrent transactions.

This article explains the actual reason behind the concurrent update issue, what PostgreSQL is doing internally, and why this behavior is necessary.

Many applications execute multiple transactions simultaneously. When two users try to modify the same row at nearly the same time, PostgreSQL must decide how to maintain data consistency.

Under lower isolation levels, such as READ COMMITTED, PostgreSQL allows transactions to see the latest committed data.

However, under stricter isolation levels such as:

  • REPEATABLE READ
  • SERIALIZABLE

PostgreSQL guarantees that a transaction always works with a consistent snapshot of the database.

The concurrent update issue occurs when PostgreSQL detects that allowing a transaction to continue would violate that guarantee.

Understanding the Scenario

Consider a product inventory table:

CREATE TABLE inventory (
    product_id INT,
    quantity INT
);
INSERT INTO inventory VALUES (1, 10);

Current quantity:

quantity = 10

Now change the value of the parameter named log_statements in postgres to all

alter system set log_statement = 'all';

Reload the postgres conf by using the below query.

select pg_reload_conf();

Result :

pg_reload_conf 
----------------
 t

Verify that the value of this parameter has changed.

show log_statement;

Result:

 log_statement 
---------------
 all

Now two transactions start almost at the same time. But start transaction 1 first, and then start transaction 2

Transaction 1 Begins

BEGIN ISOLATION LEVEL REPEATABLE READ;

Transaction 1 reads:

SELECT quantity
FROM inventory
WHERE product_id = 1;

Result:

10

At the same time, begin transaction 2

BEGIN ISOLATION LEVEL REPEATABLE READ;

Execute an update query in transaction 1

 UPDATE inventory
SET quantity = 7
WHERE product_id = 1;

Don't commit this transaction. Now, in transaction 2, execute the update query with the same where clause value

UPDATE inventory
SET quantity = 5
WHERE product_id = 1;

Now, this time, check the PostgreSQL log file.

Check the path of the PostgreSQL log file by using this command.

pg_lsclusters

Result :

Ver Cluster     Port Status Owner    Data directory                     Log file
18  main        5432 online postgres /var/lib/postgresql/18/main        /var/log/postgresql/postgresql-18-main.log

Check the live progress of the PostgreSQL log file like this.

tail -f /var/log/postgresql/postgresql-18-main.log

Result:

2026-06-11 22:13:01.011 IST [158111] postgres@postgres LOG:  statement: UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:02.012 IST [158111] postgres@postgres LOG:  process 158111 still waiting for ShareLock on transaction 5848 after 1000.110 ms
2026-06-11 22:13:02.012 IST [158111] postgres@postgres DETAIL:  Process holding the lock: 157634. Wait queue: 158111.
2026-06-11 22:13:02.012 IST [158111] postgres@postgres CONTEXT:  while updating tuple (0,3) in relation "inventory"

Now commit the transaction 1 and check the psql terminal of transaction 2 and the postgres logfile

When you commit to transaction 1, you can see an error message in transaction 2 like this.

ERROR:  could not serialize access due to concurrent update

Postgres logfile at this time.

2026-06-11 22:13:01.011 IST [158111] postgres@postgres LOG:  statement: UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:02.012 IST [158111] postgres@postgres LOG:  process 158111 still waiting for ShareLock on transaction 5848 after 1000.110 ms
2026-06-11 22:13:02.012 IST [158111] postgres@postgres DETAIL:  Process holding the lock: 157634. Wait queue: 158111.
2026-06-11 22:13:02.012 IST [158111] postgres@postgres CONTEXT:  while updating tuple (0,3) in relation "inventory"
2026-06-11 22:13:02.012 IST [158111] postgres@postgres STATEMENT:  UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:22.174 IST [157634] postgres@postgres LOG:  statement: commit;
2026-06-11 22:13:22.177 IST [157634] postgres@postgres LOG:  duration: 3.142 ms
2026-06-11 22:13:22.177 IST [158111] postgres@postgres LOG:  process 158111 acquired ShareLock on transaction 5848 after 21165.918 ms
2026-06-11 22:13:22.177 IST [158111] postgres@postgres CONTEXT:  while updating tuple (0,3) in relation "inventory"
2026-06-11 22:13:22.177 IST [158111] postgres@postgres STATEMENT:  UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:22.178 IST [158111] postgres@postgres ERROR:  could not serialize access due to concurrent update

Why PostgreSQL Rejects the Update

PostgreSQL cannot safely apply Transaction 2's update because it is based on stale data.

If PostgreSQL allowed the update:

Transaction 1:

Update happens from the value of 10 - 7

Transaction 2:

Update happens from the value of 1010 - 5

Final result:

5

The change made by Transaction 1 would effectively disappear.

This is known as a lost update problem.

To prevent this, PostgreSQL aborts Transaction 2 and raises the following:

ERROR: could not serialize access due to concurrent update

What Is Happening Internally

To understand the error, we need to look at PostgreSQL's MVCC architecture.

MVCC stands for:

Multi-Version Concurrency Control

Instead of modifying rows in place, PostgreSQL creates a new row version whenever an update occurs.

For example:

Initial row:

quantity = 10

Transaction 1 updates:

Version 1 - quantity = 10

Version 2 - quantity = 7

Transaction 2's snapshot still points to:

Version 1

while the database now considers the following:

Version 2

to be the latest committed version.

When Transaction 2 attempts to update Version 1, PostgreSQL realizes:

The row seen by Transaction 2 is no longer the current row.

Therefore, PostgreSQL aborts the transaction.

Why This Happens Only in Certain Isolation Levels

READ COMMITTED

In READ COMMITTED, every statement gets a fresh snapshot.

Example:

BEGIN;
SELECT quantity;

Result:

10

Transaction 1 commits:

7

Transaction 2 executes another statement:

SELECT quantity;

Result:

7

Since Transaction 2 sees the latest committed version, PostgreSQL can continue safely.

REPEATABLE READ

In REPEATABLE READ, a transaction always sees the same snapshot throughout its lifetime.

Snapshot taken at BEGIN

Even if another transaction commits later:

Transaction still sees old data

This can lead to conflicts when updates are attempted.

SERIALIZABLE

SERIALIZABLE is even stricter.

PostgreSQL ensures that concurrent transactions behave as if they were executed one after another.

When PostgreSQL detects that serialization is impossible, it aborts one of the transactions.

Error:

ERROR: could not serialize access due to concurrent update

Why Odoo Frequently Encounters This Error

Odoo performs many concurrent operations:

  • Stock updates
  • Sales order confirmations
  • Purchase order processing
  • Inventory adjustments
  • Accounting transactions
  • Scheduler jobs

Many users or background workers may attempt to update the same records simultaneously.

When Odoo uses transactions operating under stricter consistency guarantees, PostgreSQL may detect conflicting updates and reject one transaction.

Common examples include:

Multiple users validating stock moves

Two workers are updating the same product quantity

Concurrent modifications of accounting entries

Simultaneous scheduler executions

PostgreSQL Is Not Failing

A common misunderstanding is:

PostgreSQL failed to process the update.

This is incorrect.

What actually happened:

PostgreSQL detected a consistency violation and prevented data corruption.

The error is evidence that PostgreSQL is protecting the database.

The concurrent update error is not a bug. It is PostgreSQL's mechanism for preserving transactional consistency.

The error occurs because:

  1. Two transactions access the same row.
  2. Both read an older version of the row.
  3. One transaction commits first.
  4. The second transaction attempts to update using stale data.
  5. PostgreSQL detects the conflict.
  6. PostgreSQL aborts the second transaction to prevent a lost update.

Under REPEATABLE READ and SERIALIZABLE, PostgreSQL prioritizes consistency over blindly accepting updates. The "could not serialize access due to concurrent update" error is therefore a safety mechanism that ensures the database remains correct even under heavy concurrency.

Understanding this behavior is essential for developers working with PostgreSQL-based applications such as Odoo, where concurrent transactions are a normal part of daily operation.

whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message