Blog/SaaS

EcomLinx Inventory OS: how the SELECT FOR UPDATE lock prevents oversell

A technical deep-dive into how we use PostgreSQL row-level locking and an append-only ledger to guarantee zero oversell even during flash sales with concurrent orders across multiple channels.

GJ
Gangadhar Jena
Founder & CEO, EcomLinx · 10 Mar 2026 · 8 min read

Oversell is one of the most damaging things that can happen to a marketplace seller. On Amazon, it can trigger an account health warning. On Flipkart, a 14-day penalty window. On Meesho, ranking loss that takes weeks to recover. And beyond the platform penalties, you have an angry customer who ordered something that was never going to arrive.

Most inventory sync systems solve oversell with polling - checking stock every few minutes and pushing updates to channels. This works fine at low volume. At 500+ orders per day, during a flash sale, with orders arriving on three channels in the same second, it fails. The window between polls is wide enough to drive an oversell through.

EcomLinx Inventory OS does not poll. It uses a different approach: a database-level locking mechanism that makes it physically impossible for two transactions to read and write the same stock row simultaneously. This article explains exactly how it works.

The race condition that causes oversell

To understand why SELECT FOR UPDATE matters, you need to see the race condition that exists without it. Here is what happens when two orders arrive near-simultaneously without locking:

Without locking - the oversell scenario
T=0ms
Amazon order received for 1 unit. Available stock: 1.
T=2ms
Flipkart order received for 1 unit. Available stock (read): still 1.
T=4ms
Amazon order writes stock = 0. Commits.
T=6ms
Flipkart order reads committed stock = 0... but it already read 1 at T=2ms.
T=8ms
Flipkart order writes stock = 0. Commits. OVERSELL.

This is the classic read-modify-write race condition. Both transactions read the same stale value before either writes the updated value. Without a lock, the database has no way to prevent it.

How SELECT FOR UPDATE solves it

SELECT FOR UPDATE is a PostgreSQL statement that acquires a row-level exclusive lock at read time. Any other transaction that tries to read the same row will block until the first transaction commits or rolls back. The same scenario with locking enabled:

With SELECT FOR UPDATE - zero oversell
T=0ms
Amazon order arrives. Begins transaction. Issues SELECT FOR UPDATE on inventory row.
T=2ms
Flipkart order arrives. Tries to issue SELECT FOR UPDATE on same row. BLOCKS - waits.
T=4ms
Amazon transaction: reads stock = 1, deducts 1, writes stock = 0, commits. Lock released.
T=6ms
Flipkart transaction unblocks. Issues SELECT FOR UPDATE. Reads stock = 0.
T=8ms
Flipkart reads 0 stock. Rejects order. Returns out-of-stock error to channel API. SAFE.
The SQL, simplified
BEGIN;

-- Lock this inventory row for the duration of this transaction
SELECT quantity FROM inventory
WHERE sku_id = $1 AND seller_id = $2
FOR UPDATE;

-- If quantity > 0, deduct and confirm order
-- If quantity = 0, reject - return out-of-stock to channel API
UPDATE inventory SET quantity = quantity - 1
WHERE sku_id = $1 AND seller_id = $2 AND quantity > 0;

COMMIT; -- Lock released here

The AND quantity > 0 condition on the UPDATE is a safety net - even if two transactions somehow both get past the lock check, the second UPDATE will affect 0 rows, which we detect and treat as a rejection. Defence in depth.

The four components of Inventory OS

The SELECT FOR UPDATE lock is the core mechanism, but it works as part of a four-component system.

🗃️
The inventory ledger (PostgreSQL)

Every stock mutation is an append-only ledger entry - not an update to a single balance field. Each row records: SKU, channel, quantity delta (positive = restock, negative = sale or reservation), timestamp, and transaction ID. The current stock level is always computed as the sum of all ledger entries for a SKU. This means you have a complete audit trail of every stock change and can reconstruct the exact state at any point in time.

🔒
The reservation system

When an order is received but not yet confirmed, Inventory OS creates a soft reservation - a negative ledger entry tagged as "reserved". This reduces the available-to-sell quantity immediately, even before the order is dispatched. If the order cancels, the reservation is reversed. This prevents the window between order receipt and dispatch where a second customer could be sold the same unit.

⚖️
The allocation engine

For sellers with multiple channels, not every unit of stock should be available on every channel. The allocation engine lets you define rules: "Reserve 30 units minimum for Amazon", "Flipkart gets at most 40% of available stock". When allocations change, the engine recomputes channel-level available quantities and pushes updates to marketplace APIs - ensuring each channel only ever sees its allocated portion.

📬
The sync queue (BullMQ)

Stock changes do not go directly to marketplace APIs. They enter a Redis-backed BullMQ queue that handles retry logic, rate limiting, and deduplication. If the Flipkart API is temporarily unavailable, the job queues and retries with exponential backoff. If three stock changes arrive in quick succession for the same SKU, they are coalesced into one API call. This prevents the rate limit errors that cause stock divergence on competitor systems.

Does locking hurt performance?

The concern with row-level locking is always throughput - if Transaction B has to wait for Transaction A to commit, does that become a bottleneck at high volume?

In practice, no. Each inventory transaction is very fast - a read, a write, and a commit. At EcomLinx scale, these complete in under 5 milliseconds. The blocking time for Transaction B is measured in milliseconds. Even during a flash sale with 50 concurrent orders per second, the lock contention is imperceptible.

The alternative - no locking, with periodic reconciliation to fix oversells after the fact - is much more expensive. Every oversell triggers a cancellation workflow, a customer notification, a refund, and a marketplace penalty. The overhead of a 5ms lock is trivial compared to the operational cost of a single oversell incident.

The bottom line

Zero oversell incidents across all EcomLinx managed accounts since launch.

This is not a target or an SLA. It is the natural result of building inventory management on correct database primitives from the start. SELECT FOR UPDATE is not a clever trick - it is the standard tool for this exact problem. The surprising thing is how many inventory systems do not use it.

Want zero oversell?

Inventory OS is built on the only mechanism that guarantees it.

Book a free strategy call and we will show you how Inventory OS works for your specific channel setup - including flash sale scenarios.

7-day free trial · No credit card · Cancel anytime