What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Pessimistic locking in Rails asks the database to lock specific rows, usually with Model.lock inside a transaction or with record.with_lock, so that other transactions touching those rows wait until the first one finishes. The protection holds only when the locked read, the invariant check, and the write all happen inside that one transaction, and only for the rows the lock actually covers. The exact lock behavior comes from your database engine and version, not from Rails, so the sections below separate the Rails API from what the database does.
The race you are trying to prevent
Take a library system where each book has an available_copies count, and a checkout must never take the count below zero. Consider this simplified read-modify-write sequence:
As an Amazon Associate I earn from qualifying purchases.
- Request A loads the book and sees
available_copies = 1. - Request B loads the same book and also sees
available_copies = 1. - Request A checks that a copy is available, writes
0, and commits. - Request B checks its stale copy, which still says one copy is available, writes
0, and commits.
Two patrons now hold the same copy. Each request behaved correctly on the data it had, which is why this bug survives code review and only appears under concurrent traffic. The invariant to preserve is simple: available_copies must never go below zero, and every checkout must decrement it exactly once. The same shape appears in account balance transfers and in “only one active reservation per seat” rules.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsThe Rails pattern
The Rails Active Record Query Interface guide describes pessimistic locking as using a locking mechanism provided by the underlying database, and it documents two entry points: lock on a relation and with_lock on an existing model instance. The reference is the Active Record Query Interface guide on rubyonrails.org.
#1 Best Overall
Model.lock inside a transaction
Use this form when you are loading the record yourself. The lock request and the update belong in the same transaction:
class CheckoutService
class NoCopiesLeft < StandardError; end
def call(book_id)
Book.transaction do
book = Book.lock.find(book_id)
raise NoCopiesLeft unless book.available_copies > 0
book.update!(available_copies: book.available_copies - 1)
end
end
end
Walk through what this does:
Book.transactionopens a transaction. The lock is held until this transaction commits or rolls back.Book.lock.find(book_id)asks the database to lock the selected row while it reads it. A second request that tries to lock the same row waits (or fails, depending on the engine and its settings) until the first transaction ends.- The availability check runs against the locked row, so it cannot see a value another transaction is about to change.
update!writes the decremented count. If it raises, the transaction rolls back and the lock is released with it.
The guard clause matters as much as the lock. A lock without a check inside the critical section still lets a request decrement a count that is already zero.
with_lock on an existing instance
When you already hold a model instance, with_lock wraps the block in a transaction and reloads the record with a lock before yielding. That reload is what makes the check trustworthy: the value you test is the locked value, not the one loaded earlier in the request.
Recommended Free Tools
Rank #2
class CheckoutService
class NoCopiesLeft < StandardError; end
def call(book)
book.with_lock do
raise NoCopiesLeft unless book.available_copies > 0
book.update!(available_copies: book.available_copies - 1)
end
end
end
Use with_lock when the caller already has the object and you want the shortest correct form. Use Model.lock.find inside an explicit transaction when you want the lock visible in the code that reads the row, or when the transaction must cover other statements as well.
What the lock does in the database
Rails does not implement row locking itself. It asks the adapter for a lock, and the adapter emits SQL that the database interprets. The Rails guide’s MySQL example generates SELECT ... FOR UPDATE. That is the SQL shown for that example, not a guarantee that every adapter or database emits the same statement, and it is not a promise about how waits, lock modes, or compatibility work on PostgreSQL, SQLite, SQL Server, or a different MySQL storage engine.
Several behaviors depend on the engine and version you actually run:
Rank #3
- Lock modes and compatibility. Whether two transactions can read the same row while one holds a lock is defined by the database.
- Waiting and failure. Whether a blocked request waits, times out, or errors, and how long it waits, is set by the database and its configuration. Do not assume a timeout value; read it from your engine’s documentation and settings.
- Isolation level. The transaction isolation configuration affects what a transaction reads outside locked rows. Confirm it before you describe the behavior to your team.
- Error classes. The exception a blocked or killed transaction raises depends on the adapter and database. Check the adapter’s documented errors before writing rescue logic.
Where pessimistic locking stops protecting you
A row lock is a narrow tool. It protects the rows it locks, during the transaction that locks them. Four limits matter in production.
Coverage: the invariant must map to rows you lock
The lock in the checkout example protects one book row. If another code path decrements available_copies without taking the same lock, the invariant is not protected, regardless of how carefully the checkout path is written. Name the invariant, then list every write path that can change it.
Scope: keep the critical section short
The lock is held until the transaction ends. Calls to payment providers, email delivery, HTTP requests, or long computations inside the transaction extend the time other requests wait for the same row. The Rails guide does not prescribe a safe duration, so measure your own transactions and move non-database work outside the block where the business rules allow.
Rank #4
Deadlocks
Two transactions that lock rows in opposite orders can block each other. The Rails guide says that relations using lock are usually wrapped inside a transaction for preventing deadlock conditions, which is the Rails-level guidance. The guide does not provide a deadlock recovery policy, so any retry logic is your responsibility and should be verified against the adapter’s error classes. Where the application controls the order of locking, use one consistent order across code paths.
Uniqueness races are a different problem
Locking an existing row does not stop two requests from creating the same record. The Rails guide warns that find_or_create_by is not atomic: two requests can both find nothing and both insert. For uniqueness, the reliable control is a database unique constraint, and create_or_find_by relies on that constraint to handle the race.
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 matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Choosing between a row lock, optimistic locking, and a unique constraint
These three tools solve different races. Match the tool to the shape of the conflict.
Best Value
| Approach | Fits when | Trade-off and handling |
|---|---|---|
Pessimistic locking (lock, with_lock) |
Concurrent operations must serialize around specific existing rows, such as a stock count or balance, and some waiting is acceptable | Other transactions may wait on the locked row. Exact blocking and error behavior depends on the database engine and version. Keep the transaction short and handle database errors as your adapter documents them. |
Optimistic locking (lock_version) |
Conflicts on a record are expected to be uncommon, and a stale write can be detected and resolved | Rails uses a lock_version integer column and raises ActiveRecord::StaleObjectError when the stored version no longer matches. Your code must decide whether to retry, merge, or reject the change. |
| Unique database constraint with a create-first flow | The race concerns creating a record that must be unique on a key | Add a unique index, then use create_or_find_by, which Rails documents as handling the uniqueness race when the constraint exists. Without the constraint, the guarantee does not exist. |
No approach is a universal performance winner. The right choice depends on how often conflicts happen, what the invariant looks like, what users see when a request waits or fails, and which database runs in production.
In practice, the checkout case with high contention on a few popular titles fits pessimistic locking well. A profile edit where two admins rarely touch the same record fits lock_version better, because it avoids holding locks while a user reads a form. Account uniqueness such as one username per user belongs in a unique index, with no lock needed to prevent duplicates.
Production checklist
- Write down the invariant in one sentence, and list every row or unique key that enforces it.
- Record the Rails version, adapter, database engine and version, and transaction isolation setting before describing lock behavior to your team.
- Lock the smallest set of rows that protects the invariant, and where your code controls ordering, lock in one consistent order. This is an engineering practice, not a Rails guarantee.
- Keep the read, the invariant check, and the write inside one transaction.
- Move external calls and slow computation out of the critical section where the business rules allow.
- Decide in advance how the application responds to lock waits, deadlocks,
ActiveRecord::StaleObjectError, and constraint violations, and confirm the error classes your adapter raises. - Test concurrent requests against the same database engine and version you run in production, and monitor transaction duration and lock contention with your existing database monitoring.
The Rails guide’s statement on deadlocks is the official wording to cite when explaining why transactions surround locked relations: “Relations using lock are usually wrapped inside a transaction for preventing deadlock conditions.” It is documented in the same Active Record Query Interface guide.
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.




