20 Pattern: Pessimistic Locking
The Pessimistic Locking pattern allows a client to explicitly acquire an exclusive lock on a resource for a specified duration before modifying it.
Using functional action endpoints (e.g., POST /resources/{id}/lock and POST /resources/{id}/unlock), the API issues a temporary lock token and lease time (TTL). While locked, all write requests (PUT, PATCH, DELETE) from consumers other than the lock owner are rejected.
Important Architectural Note: In distributed web and RESTful architectures, Optimistic Locking using HTTP precondition headers (
If-Match/ETag) is almost always preferred over Pessimistic Locking. Pessimistic locking introduces statefulness, potential deadlocks, orphan lock management, and scaling bottlenecks. Only use pessimistic locking when optimistic conflict resolution is unacceptably expensive or impossible.
20.1. Overview
Pessimistic locking assumes concurrent updates will conflict and proactively blocks other consumers from making modifications while a critical operation or human workflow is underway.
Because REST is inherently stateless, pessimistic locking over HTTP relies on the Functional Resource Pattern (RPC-style action endpoints) to manage lock lifecycles:
- Acquire Lock (
POST /resources/{id}/lock): The client requests an exclusive lease for a specific number of seconds (ttlSeconds). The server responds with a uniquelockTokenand expiration timestamp. - Execute Protected Operation (
PATCH/PUT): The lock owner includes theLock-Tokenheader in subsequent modification requests. The server verifies token ownership and allows the update. - Reject Unauthenticated / Unlocked Attempts: Any request from another consumer—or requests missing the valid
Lock-Tokenheader—are rejected with423 Lockedor409 Conflict. - Release Lock (
POST /resources/{id}/unlock): The owner explicitly releases the lock when finished, or the server automatically invalidates it when the TTL expires.
20.2. When to Use Pessimistic Locking
Use Pessimistic Locking only when:
- Conflict resolution in an optimistic model is too costly.
- e.g., Holding a concert ticket seat or flight selection for 5 minutes during a multi-step checkout process where mid-checkout conflicts ruin the user experience.
- Physical or real-world operations are bound to the lock.
- e.g., Controlling physical hardware, industrial robotics, or single-access IoT devices where concurrent commands cause physical damage.
- Long-running human editing sessions on complex documents.
- e.g., Collaborative CAD modeling or legacy enterprise data forms where manual merge resolution is impractical.
20.3. When NOT to Use Pessimistic Locking
Avoid Pessimistic Locking (and use Optimistic Locking) when:
- Building scalable, high-throughput microservices or web/mobile APIs.
- Holding locks across HTTP requests forces servers to track state and severely limits horizontal scaling.
- Read-to-write ratios are high.
- Standard CRUD operations should never hold pessimistic locks. Use
If-Matchwith ETags to detect collision at the moment of update.
- Standard CRUD operations should never hold pessimistic locks. Use
- Clients cannot guarantee clean disconnects.
- Mobile applications on flaky networks frequently drop connections. Unreleased locks starve other consumers until TTL timeouts expire.
20.4. What the Pattern Looks Like
Below are detailed HTTP interaction flows showing how a client acquires a lock, performs an update, blocks concurrent clients, and unlocks the resource.
1. Client 1 Acquires Lock (Functional Endpoint)
Client 1 requests a 300-second (5-minute) lease on an order resource.
Request
POST /orders/ord_99182/lock HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
{
"ttlSeconds": 300,
"reason": "Seat reservation checkout"
}
Response
The server returns a unique lock token and expiration timestamp.
HTTP/1.1 201 Created
Lock-Token: lk_tok_88291a
Content-Type: application/json
{
"orderId": "ord_99182",
"lockToken": "lk_tok_88291a",
"lockedBy": "usr_client_1",
"expiresAt": "2026-08-03T14:35:00Z",
"ttlRemainingSeconds": 300
}
2. Client 1 Successfully Updates Resource (Owner)
Client 1 passes the Lock-Token header to execute the update.
Request
PATCH /orders/ord_99182 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Lock-Token: lk_tok_88291a
Content-Type: application/json
{
"seatNumber": "12A",
"status": "CONFIRMED"
}
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "ord_99182",
"seatNumber": "12A",
"status": "CONFIRMED",
"updatedAt": "2026-08-03T14:31:00Z"
}
3. Client 2 Attempted Update (Rejected with 423 Locked)
Client 2 attempts to modify the same resource without the valid lock token.
Request
PATCH /orders/ord_99182 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
{
"seatNumber": "12A"
}
Response
The server rejects the update because the resource is locked by Client 1.
HTTP/1.1 423 Locked
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/resource-locked",
"title": "Resource Locked",
"status": 423,
"detail": "The order 'ord_99182' is currently locked by another consumer until 2026-08-03T14:35:00Z.",
"lockedUntil": "2026-08-03T14:35:00Z"
}
4. Client 1 Releases Lock (Functional Endpoint)
Once finished, Client 1 explicitly releases the lock.
Request
POST /orders/ord_99182/unlock HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Lock-Token: lk_tok_88291a
Response
HTTP/1.1 204 No Content
20.5. Anti-Patterns to Avoid
1. Indefinite locks without TTL expiration
- Creating locks that never expire means a single client crash or network disconnection permanently freezes the resource until manual database intervention occurs. Always enforce a hard
ttlSecondscap server-side.
2. Using pessimistic locking when optimistic locking works
- Introducing lock/unlock endpoints for standard data editing burdens clients with state management and invites deadlocks. Always prefer Optimistic Locking (
If-Match/ETag) unless business rules strictly forbid conflict resolution.
3. Passing lock tokens in request JSON bodies
- Mixing protocol control tokens into domain payload objects pollutes application models. Pass lock tokens in custom HTTP headers (e.g.,
Lock-TokenorX-Lock-Token).
4. Non-atomic lock acquisitions
- Checking if a resource is locked in one database query and inserting the lock record in another introduces race conditions. Ensure lock creation is executed in a single atomic database operation or distributed lock manager (e.g., Redis
SET NX EX).
20.6. OpenAPI Example
A complete OpenAPI 3.0.3 specification illustrating how to document functional lock/unlock endpoints, the Lock-Token header, and the 423 Locked error response.
openapi: 3.0.3
info:
title: Order Management API - Pessimistic Locking Example
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/orders/{orderId}/lock:
post:
summary: Acquire exclusive lock on an order
description: Issues a temporary lock token and lease time. Blocks write access for other consumers.
tags: [Order Locking]
parameters:
- in: path
name: orderId
required: true
schema:
type: string
example: ord_99182
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- ttlSeconds
properties:
ttlSeconds:
type: integer
default: 300
maximum: 900
example: 300
reason:
type: string
example: "Seat reservation checkout"
responses:
'201':
description: Lock successfully acquired
headers:
Lock-Token:
schema:
type: string
example: lk_tok_88291a
description: Token required in subsequent update requests.
content:
application/json:
schema:
$ref: '#/components/schemas/LockResponse'
'409':
description: Resource is already locked by another client
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
/orders/{orderId}/unlock:
post:
summary: Release an existing lock
description: Releases the exclusive lock using the issued Lock-Token.
tags: [Order Locking]
parameters:
- in: path
name: orderId
required: true
schema:
type: string
example: ord_99182
- in: header
name: Lock-Token
required: true
schema:
type: string
example: lk_tok_88291a
responses:
'204':
description: Lock released successfully
'403':
description: Invalid or expired lock token
/orders/{orderId}:
patch:
summary: Update order (requires Lock-Token if locked)
tags: [Orders]
parameters:
- in: path
name: orderId
required: true
schema:
type: string
example: ord_99182
- in: header
name: Lock-Token
required: false
schema:
type: string
example: lk_tok_88291a
description: Required if the order is currently locked.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
seatNumber:
type: string
example: "12A"
status:
type: string
example: "CONFIRMED"
responses:
'200':
description: Order updated successfully
'423':
description: Resource Locked - Order is locked by another client
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
components:
schemas:
LockResponse:
type: object
properties:
orderId:
type: string
example: ord_99182
lockToken:
type: string
example: lk_tok_88291a
lockedBy:
type: string
example: usr_client_1
expiresAt:
type: string
format: date-time
example: "2026-08-03T14:35:00Z"
ttlRemainingSeconds:
type: integer
example: 300
ProblemDetails:
type: object
properties:
type:
type: string
example: "https://api.example.com/errors/resource-locked"
title:
type: string
example: "Resource Locked"
status:
type: integer
example: 423
detail:
type: string
example: "The order is currently locked by another consumer."
20.7. Visualizing Pessimistic Locking (Mermaid Diagram)
sequenceDiagram
autonumber
actor Client1 as Client 1 (Lock Owner)
actor Client2 as Client 2 (Other Consumer)
participant API as API Gateway / Server
participant LockMgr as Distributed Lock Manager
participant DB as Database
Note over Client1,DB: Phase 1: Client 1 Acquires Lock
Client1->>API: POST /orders/ord_99182/lock { ttlSeconds: 300 }
API->>LockMgr: Acquire lock for ord_99182 (TTL: 300s)
LockMgr-->>API: Lock granted (Token: "lk_tok_88291a")
API-->>Client1: 201 Created (Lock-Token: "lk_tok_88291a", expiresAt)
Note over Client1,DB: Phase 2: Client 2 Attempt Blocked
Client2->>API: PATCH /orders/ord_99182 (No Lock-Token)
API->>LockMgr: Check lock status for ord_99182
LockMgr-->>API: Active Lock Held by Client 1!
API-->>Client2: 423 Locked ("Resource is locked by another consumer")
Note over Client1,DB: Phase 3: Client 1 Updates Resource
Client1->>API: PATCH /orders/ord_99182<br/>Header: Lock-Token: "lk_tok_88291a"
API->>LockMgr: Validate Lock-Token "lk_tok_88291a"
LockMgr-->>API: Token Valid
API->>DB: UPDATE orders SET seat_number = '12A' WHERE id = 'ord_99182'
DB-->>API: Update Success
API-->>Client1: 200 OK (Updated Order)
Note over Client1,DB: Phase 4: Client 1 Releases Lock
Client1->>API: POST /orders/ord_99182/unlock<br/>Header: Lock-Token: "lk_tok_88291a"
API->>LockMgr: Delete lock for ord_99182
LockMgr-->>API: Lock Released
API-->>Client1: 204 No Content