What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
Yes—an update can have a race condition even when the database ends with one valid row. Two requests may both read the same old value and each trigger a side effect, or a row may be deleted after it is read so a later save affects no row while application code continues as if it did. In Django, transaction.atomic() groups changes for commit or rollback; it does not, by itself, serialize every read–decide–write sequence. The practical question is not just whether the final row is valid, but whether the whole operation—including notifications and other effects—respects the intended invariant.
How an update race can leave the row correct but the operation wrong
Consider a Django workspace membership whose role is MEMBER. Two concurrent requests both ask to change it to ADMIN. If each request reads the old role before either saves, both can decide that a role change occurred. The database may finish with one membership row set to ADMIN, exactly the desired row state, while both requests emit a WORKSPACE_ROLE_CHANGED notification.
Majid Khazaei describes this scenario in an audit of a Django WorkspaceService: the unprotected change_member_role path did a regular membership lookup, then mutated the record and sent a notification. The duplicate notification is an author-reported incident, not an independently reproduced result. It illustrates why checking only the final database row can miss a concurrency defect: related side effects can be duplicated even when the row itself is not corrupt.
Recommended Free Tools
Why atomic() alone is not a serialization guarantee
Django’s transaction documentation describes atomic() as a way to commit database changes in a block together or roll them back when an exception exits the block. That protects transactional integrity, but it does not automatically make a sequence of “read current value, decide what to do, then write” exclusive. Two transactions can each make a decision based on a value read before the other transaction’s change is visible to that decision.
#1 Best Overall
For an invariant that depends on a row’s current state, the code needs a concurrency strategy that protects the decision as well as the write. A row lock is one option; an atomic conditional update may suit other invariants. The correct choice depends on whether the invariant concerns only stored row state or also a related side effect.
How a concurrent delete can make a save a no-op
A second hazard occurs when one request reads a membership, another request deletes it, and the first later attempts to save its in-memory object. Khazaei’s article reports that in the described Django path, this save can update zero rows without raising an exception, while the first request still proceeds to notification code. That outcome is specific to the described ORM/code path; it should not be assumed for every ORM or every save operation.
The key risk is a mismatch between what the application believes it changed and what the database actually matched. If later code treats a save call as proof that a row still existed and was updated, it can send a misleading notification or perform another action on a stale assumption. Make the intended invariant explicit and ensure the mutation’s result and any dependent effect are coordinated.
Protecting a read–decide–write operation with a row lock
On a supported backend, Django’s select_for_update() documentation says the queryset generates a SELECT ... FOR UPDATE query and locks selected rows until the transaction ends. A conflicting lock attempt normally waits for the other transaction to release its lock. Khazaei reports changing the role and removal paths to acquire a lock when fetching the membership, so a competing request cannot make its decision using the same unlocked row version. In the same-role case, the later request can observe the committed role and take a no-op path instead of generating a second role-change notification.
Rank #3
- Open a transaction. Evaluate the lock-taking query inside
transaction.atomic()on a backend that supports it. - Fetch the row to be protected. For example, use
Membership.objects.select_for_update().get(...)with the membership’s identifying conditions. - Decide and mutate while holding the lock. Compare the current role with the requested role and make the change only if the invariant says it is a real change.
- Coordinate dependent effects. If a notification should happen only after a successful commit, Django’s
transaction.on_commit()can schedule the callback after commit. The callback itself is not part of the database transaction, so a delivery system that must be reliable across process failure needs an additional durable design.
This pattern makes the ordering explicit for a role-change/delete race: whichever transaction obtains the row lock first proceeds, and the other waits. After the first transaction commits, the second must contend with the resulting state—either the row remains and can be read, or it has been deleted and is missing. The row lock protects the selected row during the transaction; it does not automatically protect unrelated rows or non-database systems.
Backend support and lock options
Django 5.2 documents support for select_for_update() on PostgreSQL, Oracle, and MySQL, with option differences for MySQL and MariaDB. On SQLite, the method has no effect and adds no locking clause. On a supporting backend, evaluating it outside a transaction raises TransactionManagementError. These differences matter both in production and in tests: a local SQLite run does not demonstrate PostgreSQL row-lock behavior.
Rank #4
Django also documents two options that change conflict behavior: nowait=True requests an immediate database error rather than waiting, while skip_locked=True omits rows already locked by another transaction. They are not interchangeable fixes. Choose the behavior the application can handle—wait, fail and report/retry, or skip work—and design the surrounding code accordingly.
Free tools Windows power users keep installed
One-click scans. No signup required.
Lock scope, transaction length, and deadlocks
Locks have a cost: conflicting work can wait, and open transactions consume database resources. Lock only the rows needed to protect the invariant, and keep the transaction short. Avoid network calls or slow external work while holding a database lock. Django’s transaction guidance also recommends catching database errors around an atomic block rather than hiding them inside the block, and notes that on_commit() callbacks run only after a successful commit.
Best Value
- Used Book in Good Condition
If one operation must lock multiple rows, acquire them in a consistent order. PostgreSQL 18’s explicit-locking documentation explains that transactions taking conflicting locks in opposing orders can deadlock; PostgreSQL detects a deadlock and aborts one participant, and which one is aborted is difficult to predict. Consistent ordering is the general defense. A single-row lock in one particular removal path may avoid that specific multi-row pattern, but it is not a universal promise that an application cannot deadlock.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Writing concurrency tests around invariants
A concurrency test should assert what must remain true, not assume which thread or transaction the scheduler will favor. Khazaei reports using real threads and transactional tests for the examples below; the outcomes are attributed to the article and were not independently run here. For Django locking tests, the QuerySet documentation recommends TransactionTestCase, because ordinary TestCase wraps each test in a transaction and can mask whether a lock query is being evaluated in the required transaction context.
| Concurrent operations | Contract to assert |
|---|---|
| Two requests set the same role | Both requests may complete; one membership remains with the requested role; exactly one role-change notification is expected; no integrity error occurs. |
| Two requests set different roles | The winner is nondeterministic. One membership remains, its role is one of the two requested roles, and no integrity error occurs. |
| Role change and removal | The final membership count is zero whichever operation takes effect first. Khazaei used an owner actor for an order-independent test, since an admin actor could be blocked by the changed role depending on transaction order. |
For a robust test, arrange for the operations to overlap using separate database connections and then check the invariant after both finish. Do not assert a particular winner where both orders are valid. Also assert side-effect counts, because a correct final row does not prove that notifications were emitted exactly once.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallChoosing a concurrency strategy
A row lock is useful when code must inspect a current row and make a decision that depends on that state. A conditional update can be a better fit when the invariant can be expressed in one database operation, such as “change this row only if its role is still X”; the application must still handle the affected-row count and coordinate any dependent side effect. Whichever approach is chosen, verify backend support, lock scope, conflict handling, and whether the invariant extends beyond the row itself.
The audit lesson in Khazaei’s article is to check sibling operations, not only the create path where a race was first found. A role change and a removal may make different assumptions about row existence and current state, so each read–decide–write path deserves an invariant-focused review.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

