Last updated: August 25, 2026.
Record locking prevents two people from unknowingly overwriting the same database row. In a web application, a lock cannot depend on the browser staying connected: requests are short-lived, tabs close without warning, and networks disappear. A practical design combines a unique lock, a short expiration, and a heartbeat.
Choose the locking model first
- Optimistic locking allows simultaneous editing but rejects a save when a version number or timestamp changed. It works well when collisions are rare.
- Pessimistic locking reserves a record while someone edits it. It helps when collisions are costly, but abandoned locks must expire.
The rest describes a pessimistic application-level lock. Do not hold a database transaction open while a person edits a form.
Lock-table structure
Use one unique key per editable resource and a random owner token for each editing instance.
CREATE TABLE record_locks (
resource_key VARCHAR(191) PRIMARY KEY,
owner_token CHAR(64) NOT NULL,
acquired_at DATETIME(6) NOT NULL,
refreshed_at DATETIME(6) NOT NULL,
expires_at DATETIME(6) NOT NULL,
INDEX (expires_at)
);The primary key makes acquisition atomic: two requests cannot insert the same resource_key. Remove an expired row inside a short transaction, attempt the insert, and treat a duplicate-key result as “already locked.”
Safe locking workflow
- Acquire: create an owner token, remove an expired lock if necessary, and insert the new lock.
- Edit: refresh timestamps every few seconds, only when both the resource key and owner token match.
- Save: verify the token and update the business record in the same short transaction. Optionally include a record version as optimistic protection.
- Release: delete the lock when editing ends. Expiration remains the fallback.
Heartbeat example
The interval must be comfortably shorter than expiration—for example, refresh every 10 seconds and expire after 30–60 seconds.
const heartbeat = setInterval(async () => {
const response = await fetch('/api/locks/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resourceKey, ownerToken })
});
if (!response.ok) {
clearInterval(heartbeat);
disableSaveButton();
showMessage('The edit lock was lost. Reload before saving.');
}
}, 10_000);Use pagehide or navigator.sendBeacon() for best-effort release, but never depend on it.

Failure cases to design for
- Lost connection: stop saving when a heartbeat fails; reload current data before requesting a new lock.
- Long-open form: enforce a maximum duration or let an administrator release stale locks.
- Multiple tabs: use a different owner token for each editing instance.
- Clock differences: calculate times in the database or one trusted server.
- Authorization: owning a lock does not grant permission; check access again at save time.

Legacy downloadable example
The original record-locking sample project is retained for reference. It predates these recommendations, so review and test it before using any part in a current application.