In distributed web systems, the most efficient network transaction is the one that never happens. Caching is not an optional optimization or an afterthought in REST; it is codified as the third foundational constraint in Roy Fielding's architectural dissertation. By designating responses as explicitly cacheable or non-cacheable, systems eliminate redundant network round-trips, dramatically reduce origin server CPU and database load, and slash global client-perceived latency from hundreds of milliseconds to sub-millisecond local lookups.
However, caching is notoriously double-edged. Misconfigured cache directives can leak Personally Identifiable Information (PII) to public proxy caches, cause stale data anomalies that corrupt business decisions, or trigger massive "thundering herds" that crash database clusters upon cache expiration. This module delivers an exhaustive, production-grade architectural analysis of HTTP caching mechanisms (RFC 7234, RFC 9111), validation primitives (RFC 7232), optimistic concurrency control via entity tags (ETag), and cache key coordination with the Vary header.
The Distributed Caching Topology#
To architect caching effectively, an engineer must distinguish between the discrete physical layers where representations are cached throughout the network path.
+-----------------------------------------------------------------------------------+
| End-to-End Distributed Caching Topology |
| |
| [ Client Browser / Native App ] |
| - Private Local Cache (Disk / Memory) |
| - Strictly isolated to a single authenticated user |
| | |
| v (Public Internet / WAN) |
| +-----------------------------------------------------------------------------+ |
| | CDN Edge Points of Presence (PoPs) (Cloudflare / Fastly / Akamai) | |
| | - Shared Edge Cache (Distributed globally) | |
| | - Evaluates 's-maxage', 'stale-while-revalidate', 'Cache-Tags' | |
| +-------------------------------------+---------------------------------------+ |
| | Cache Miss / Revalidation |
| v |
| +-----------------------------------------------------------------------------+ |
| | Reverse Proxy / API Gateway (Nginx / Varnish / Envoy) | |
| | - Shared Internal Cache (Protects Origin Cluster) | |
| | - Micro-caching (1-5 second TTLs for high-volume spikes) | |
| +-------------------------------------+---------------------------------------+ |
| | Cache Miss |
| v |
| +-----------------------------------------------------------------------------+ |
| | Origin Application Servers & Database Cluster | |
| | - Computes representation, generates ETag, queries PostgreSQL / Redis | |
| +-----------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------+1. Private Caches (Client-Side)#
A private cache belongs exclusively to a single end-user (e.g., the browser's HTTP cache or an on-device SQLite cache in a mobile app). Private caches are permitted to store user-specific, sensitive representations (such as account profiles, shopping carts, and private messages).
2. Shared Caches (Intermediary & Edge)#
Shared caches reside between the client and the origin server and store responses intended for reuse across multiple heterogeneous users. These include:
- Content Delivery Networks (CDNs) deployed at thousands of edge points-of-presence globally.
- Corporate forward proxies.
- Reverse proxies and API gateways deployed in front of origin application clusters.
Strict Security Rule: A shared cache must never store responses that are authenticated or contain private user data unless explicitly instructed via the public directive and verified to exclude user-specific identity tokens.
The Cache-Control Directives & Semantics#
The modern HTTP caching contract is governed by the Cache-Control header (standardized in RFC 7234 and updated in RFC 9111). Understanding the exact formal behavior of each directive is essential for preventing both stale data bugs and unintended security exposures.
+------------------------------------+------------------+-------------------------------------------------------+
| Directive | Target Caches | Precise Architectural Semantics |
+------------------------------------+------------------+-------------------------------------------------------+
| max-age=<seconds> | Private & Shared | Maximum time a representation is considered "fresh". |
| s-maxage=<seconds> | Shared Only | Overrides max-age for shared caches (CDNs/proxies). |
| no-cache | Private & Shared | MUST revalidate with origin before serving (304 check).|
| no-store | Private & Shared | Absolute ban on caching; never write bytes to disk. |
| public | Private & Shared | Explicit permission for shared caches to store data. |
| private | Private Only | Excludes shared caches; only browser may store data. |
| must-revalidate | Private & Shared | Stale responses CANNOT be served under any condition. |
| proxy-revalidate | Shared Only | Stale responses CANNOT be served by shared proxies. |
| immutable | Private & Shared | Representation will never change; zero 304 checks. |
| stale-while-revalidate=<seconds> | Private & Shared | Serve stale instantly while refreshing in background. |
| stale-if-error=<seconds> | Private & Shared | Serve stale if origin returns 5xx server error. |
+------------------------------------+------------------+-------------------------------------------------------+Deep Dive: The Critical Distinction Between `no-cache` and `no-store`#
One of the most catastrophic and common bugs in API engineering is conflating no-cache with no-store.
`Cache-Control: no-cache`#
- What it means: "You may store this response in cache, but you MUST NOT serve it to satisfy a subsequent request without first revalidating with the origin server (using
If-None-MatchorIf-Modified-Since)." - Behavior: If the origin server verifies that the data has not changed, it returns
304 Not Modified. The cache then serves the stored representation.no-cachesaves network bandwidth, but still requires a server round-trip to verify freshness.
`Cache-Control: no-store`#
- What it means: "Do not store any part of this request or response in any cache anywhere under any circumstances."
- Behavior: The response bytes must not be written to disk, flash storage, or persistent cache memory. It is mandated for payment card data, banking transactions, healthcare records, and authentication tokens.
If an API developer attaches no-cache to a credit card endpoint believing it prevents caching, a proxy cache may persist the credit card payload to disk storage, creating a severe PCI-DSS compliance violation!
Edge CDN Optimization: `stale-while-revalidate`#
In high-concurrency microservices, network latency can be virtually eliminated using the stale-while-revalidate directive:
Cache-Control: max-age=60, stale-while-revalidate=300
Timeline (Seconds):
0s ---------------- 60s ------------------------------ 360:
| FRESH WINDOW | STALE-WHILE-REVALIDATE | EXPIRED
| | |
| Served instantly | Client arrives: | Client arrives:
| from edge cache | 1. Instantly served stale payload | Synchronous origin
| (0 ms latency). | (0 ms latency). | fetch (Blocking).
| | 2. Edge triggers asynchronous |
| | background fetch to origin. |
| | 3. Cache updated transparently. |- During the first 60 seconds, the response is fresh and served from edge cache in under 10 ms.
- Between 60s and 360s, the response is technically stale. However, when a client arrives, the CDN immediately returns the stale representation, ensuring the user experiences instantaneous response time (P99 < 15 ms).
- Simultaneously, the CDN spawns an asynchronous background thread to revalidate with the origin server and refresh the cache for future callers.
- This completely eliminates the "cache stampede" or thundering herd problem where thousands of concurrent requests slam the origin database the exact instant a TTL expires.
Validation Primitives: Strong vs Weak Validators#
When a cached representation exceeds its freshness lifetime (max-age), the cache must revalidate with the origin server to determine if the representation is still valid. This is executed using Conditional Requests (RFC 7232).
+-----------------------------------------------------------------------------------+
| Conditional Request Revalidation Flow |
| |
| [ Client / CDN Proxy ] [ Origin Server ] |
| | | |
| | Initial Request: GET /v1/products/42 | |
| |--------------------------------------------------->| |
| | | Compute response |
| | 200 OK | Generate ETag: |
| | ETag: "a8f3b9" | "a8f3b9" |
| | Cache-Control: max-age=300, no-cache | |
| |<---------------------------------------------------| |
| | | |
| | [ 300 Seconds Expire ] | |
| | | |
| | Subsequent Request: GET /v1/products/42 | |
| | If-None-Match: "a8f3b9" | |
| |--------------------------------------------------->| |
| | | Check: |
| | | Current ETag == |
| | | "a8f3b9"? YES! |
| | 304 Not Modified | |
| | (Empty Body, 0 Bytes Payload) | |
| |<---------------------------------------------------| |
| | | |
+-----------------------------------------------------------------------------------+Strong vs Weak ETags#
An Entity Tag (ETag) is an opaque validator generated by the origin server representing the specific state of a resource representation.
1. Strong ETags#
- Format:
ETag: "686897696a7c876b7e" - Guarantee: Byte-for-byte exact equality. If two representations share the same strong ETag, every single octet in the response body is identical. Strong ETags are required for byte-range requests (
Range: bytes=0-1024).
2. Weak ETags#
- Format:
ETag: W/"10834-semantic-v2" - Guarantee: Semantic equivalence. The two representations convey the exact same domain meaning, even if minor byte-level formatting differences exist (e.g., whitespace formatting, JSON attribute ordering, or different date format encodings). Weak ETags cannot be used for byte-range resumption.
Revalidation Headers Comparison#
| Conditional Request Header | Matching Response Header | Primary Use Case | Granularity |
|---|---|---|---|
If-None-Match: "<ETag>" | ETag | Cache freshness validation (304) | Exact hash / Version |
If-Modified-Since: <Date> | Last-Modified | Legacy cache validation (304) | 1-Second granularity |
If-Match: "<ETag>" | ETag | Optimistic Concurrency (412) | Exact hash / Version |
If-Unmodified-Since: <Date> | Last-Modified | Conditional update lock (412) | 1-Second granularity |
Optimistic Concurrency Control (Lost Update Prevention)#
In distributed enterprise systems, concurrent writes to the same resource inevitably lead to race conditions. The classic failure mode is the Lost Update Problem.
+-----------------------------------------------------------------------------------+
| The Lost Update Problem vs Optimistic Concurrency Control |
| |
| Client A Server (Balance: 100) Client B |
| | | | |
| | GET /v1/account | | |
| | (Reads Balance: 100, ETag: "v1") | | |
| |----------------------------------->| | |
| | | GET /v1/account | |
| | | (Reads Balance: 100, | |
| | | ETag: "v1") | |
| | |<----------------------------| |
| | | | |
| | Client A adds 50 (Target: 150) | | |
| | PUT /v1/account { balance: 150 } | | |
| | If-Match: "v1" | | |
| |----------------------------------->| | |
| | 200 OK (New ETag: "v2") | Server checks ETag "v1". | |
| |<-----------------------------------| Updates balance to 150. | |
| | | Current ETag is now "v2". | |
| | | | |
| | | Client B subtracts 20 | |
| | | (Target: 80) | |
| | | PUT /v1/account { bal: 80 } | |
| | | If-Match: "v1" | |
| | |<----------------------------| |
| | | Server checks ETag "v1". | |
| | | MISMATCH! Current is "v2". | |
| | | 412 Precondition Failed | |
| | |---------------------------->| |
| | | | |
+-----------------------------------------------------------------------------------+The Mechanism of `If-Match`#
- When a client performs a mutating operation (
PUT,PATCH,DELETE), it includes the header:
If-Match: "v1"
- The server compares the provided token with the current resource state in the database.
- If the tokens match, the server executes the update, increments the version/hash, and returns
200 OKor204 No Contentwith the newETag: "v2". - If a competing client updated the record in the interim, the server detects the mismatch and immediately aborts the transaction with HTTP 412 Precondition Failed.
- Client B's update is safely rejected rather than silently overwriting Client A's data. Client B is instructed to re-fetch the latest state, re-evaluate business logic, and retry.
Content Negotiation Caching & The `Vary` Header#
A single URI may serve different representations depending on the client's request headers:
- Compression:
Accept-Encoding: gzipvsbr(Brotli) vs identity (uncompressed). - Data Format:
Accept: application/jsonvsapplication/xml. - Language:
Accept-Language: fr-CAvsen-US.
If a shared intermediary cache (such as an edge CDN) caches a Brotli-compressed response for /v1/products and subsequently serves that binary blob to an old legacy HTTP client that does not support compression, the client crashes.
The Role of the `Vary` Header#
The Vary header instructs caches which request headers must be included as part of the Cache Key:
Vary: Accept-Encoding, Accept
Under this header, the cache constructs a composite cache key:
Cache Key = MD5(URL + Method + Accept-Encoding + Accept)### The "Cache Key Explosion" Trap
Developers must be cautious not to vary on headers with massive cardinality. For example, adding Vary: User-Agent means that every distinct browser version, OS patch, and mobile device string generates a completely separate cache entry. A CDN with 50,000 distinct user agents will experience a near-zero cache hit ratio (< 1\%), completely destroying cache efficiency.
Production Optimistic Concurrency & Caching Implementation#
Below is a complete, production-grade TypeScript/Node.js implementation of an Express controller featuring deterministic SHA-256 ETag generation, conditional 304 Not Modified handling, and atomic If-Match optimistic concurrency control.
// inventory-controller.ts
import { Request, Response } from 'express';
import crypto from 'node:crypto';
export interface InventoryItem {
id: string;
sku: string;
quantity: number;
warehouseLocation: string;
version: number;
updatedAt: string;
}
// In-memory mock database with version tracking
const inventoryDb = new Map<string, InventoryItem>([
[
'item_101',
{
id: 'item_101',
sku: 'MACBOOK-PRO-M3-32GB',
quantity: 45,
warehouseLocation: 'US-WEST-A3',
version: 1,
updatedAt: '2026-10-30T10:00:00Z'
}
]
]);
export class InventoryController {
// Generates a deterministic strong ETag from entity content and version
private static computeETag(item: InventoryItem): string {
const payload = `{item.id}:{item.version}:{item.quantity}:{item.warehouseLocation}`;
const hash = crypto.createHash('sha256').update(payload).digest('hex').slice(0, 16);
return `"{item.version}-{hash}"`;
}
// GET /v1/inventory/:id (Handles Caching & 304 Validation)
public static async getItem(req: Request, res: Response): Promise<void> {
const itemId = req.params.id;
const item = inventoryDb.get(itemId);
if (!item) {
res.status(404).json({ error: 'Item not found' });
return;
}
const currentETag = InventoryController.computeETag(item);
const ifNoneMatch = req.get('If-None-Match');
// Standard Cache Headers
res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
res.setHeader('ETag', currentETag);
res.setHeader('Vary', 'Accept-Encoding');
// Conditional Validation: Check if client cache is still fresh
if (ifNoneMatch && (ifNoneMatch === currentETag || ifNoneMatch === '*')) {
// Return 304 Not Modified without response body
res.status(304).end();
return;
}
res.status(200).json(item);
}
// PATCH /v1/inventory/:id (Handles Optimistic Concurrency via If-Match)
public static async updateStock(req: Request, res: Response): Promise<void> {
const itemId = req.params.id;
const item = inventoryDb.get(itemId);
if (!item) {
res.status(404).json({ error: 'Item not found' });
return;
}
const ifMatch = req.get('If-Match');
const currentETag = InventoryController.computeETag(item);
// Mandatory Optimistic Concurrency Precondition Check
if (!ifMatch) {
res.status(428).json({
type: 'https://api.devthesis.com/errors/precondition-required',
title: 'Precondition Required',
status: 428,
detail: 'Updates to inventory stock require an If-Match header with current ETag.'
});
return;
}
// Verify ETag Match
if (ifMatch !== currentETag && ifMatch !== '*') {
res.status(412).json({
type: 'https://api.devthesis.com/errors/precondition-failed',
title: 'Precondition Failed: Resource Version Conflict',
status: 412,
detail: 'The inventory item was modified by another transaction. Please re-fetch and retry.',
currentETag: currentETag
});
return;
}
// Apply partial mutation
const { quantityDelta, warehouseLocation } = req.body;
if (typeof quantityDelta === 'number') {
if (item.quantity + quantityDelta < 0) {
res.status(409).json({ error: 'Insufficient inventory quantity' });
return;
}
item.quantity += quantityDelta;
}
if (warehouseLocation) {
item.warehouseLocation = warehouseLocation;
}
// Increment version & update timestamp
item.version += 1;
item.updatedAt = new Date().toISOString();
inventoryDb.set(itemId, item);
const newETag = InventoryController.computeETag(item);
res.setHeader('ETag', newETag);
res.status(200).json(item);
}
}Checkpoint Practice Problems#
Apply the formal principles of distributed caching, validation semantics, and concurrency control to solve these real-world engineering problems.
Problem 1: CDN Hit-Ratio & Origin Bandwidth Reduction Analysis#
A media streaming API serves dynamic user playlist summaries. The origin server cluster receives an average incoming request volume of R = 50,000 requests/sec (RPS). The uncompressed JSON playlist summary response payload size is S = 64 KB (65,536 bytes).
The platform configures an edge CDN tier. Profiling across global edge PoPs reveals that the Cache Hit Ratio (H) is a function of the configured Cache-Control: max-age=T according to the empirical saturation model:
H(T) = (T) / (T + 30)where T is the cache TTL in seconds.
The engineering team evaluates three candidate caching policies:
- Policy A: No caching (T = 0 s).
- Policy B: Short Micro-caching (T = 10 s).
- Policy C: Aggressive Edge Caching with Revalidation (T = 120 s).
Calculate:
- The Cache Hit Ratio (H) under Policies A, B, and C.
- The outbound network bandwidth consumed at the origin datacenter in Megabits per second (Mbps) for each policy.
- Compute the monthly network egress cost reduction under Policy C assuming an origin bandwidth cost of \0.08 per Gigabyte (1 GB = 10^9 bytes, 30-day month).
Solution:
Step 1: Calculate Cache Hit Ratio (H):
- Policy A (T = 0):
H(0) = (0) / (0 + 30) = 0.00 (0% Hit Ratio) Policy B (T = 10)*:
H(10) = (10) / (10 + 30) = (10) / (40) = 0.25 (25% Hit Ratio) Policy C (T = 120)*:
H(120) = (120) / (120 + 30) = (120) / (150) = 0.80 (80% Hit Ratio)Step 2: Calculate Origin Outbound Bandwidth:
Origin Request Rate = R_{origin} = R × (1 - H).
Origin Bandwidth (Bytes/sec) = R_{origin} × S.
Origin Bandwidth (Bits/sec) = Bytes/sec × 8.
- Policy A (H = 0):
R_{origin} = 50,000 × (1 - 0) = 50,000 req/sec.
Bandwidth = 50,000 × 65,536 bytes/sec = 3,276,800,000 bytes/sec.
Bandwidth in Mbps = (3,276,800,000 × 8) / (10^6) = 26,214.4 Mbps ≈ 26.21 Gbps.
- Policy B (H = 0.25):
R_{origin} = 50,000 × (1 - 0.25) = 37,500 req/sec.
Bandwidth = 37,500 × 65,536 × 8 / 10^6 = 19,660.8 Mbps ≈ 19.66 Gbps.
- Policy C (H = 0.80):
R_{origin} = 50,000 × (1 - 0.80) = 10,000 req/sec.
Bandwidth = 10,000 × 65,536 × 8 / 10^6 = 5,242.88 Mbps ≈ 5.24 Gbps.
Step 3: Monthly Egress Cost Savings (Policy C vs Policy A):
Seconds per 30-day month = 30 × 24 × 3600 = 2,592,000 seconds.
Total data egress under Policy A:
Data_A = 3.2768 × 10^9 B/s × 2,592,000 s = 8.4934 × 10^{15} bytes ≈ 8,493,465 GB.
Monthly Cost(A) = 8,493,465 × \0.08 = \679,477.20.
Total data egress under Policy C (80\% reduction):
Data_C = 0.20 × Data_A ≈ 1,698,693 GB.
Monthly Cost(C) = 1,698,693 × \0.08 = \135,895.44.
Monthly Financial Savings = \679,477.20 - \135,895.44 = \543,581.76 per month.
Configuring Policy C offloads 21 Gbps of traffic, saving over \543,000 per month in infrastructure egress costs while offloading 80% of database compute queries.
Problem 2: ETag Hash Collision Probability & Birthday Paradox#
An API gateway generates strong entity tags (ETag) by hashing response representations. Two hashing algorithms are evaluated for a catalog containing N = 50,000,000 active products:
- Algorithm 1: A 32-bit CRC32 checksum (m = 32 bits, hash space K_1 = 2^{32} ≈ 4.29 × 10^9).
- Algorithm 2: A truncated 64-bit MurmurHash3 (m = 64 bits, hash space K_2 = 2^{64} ≈ 1.84 × 10^{19}).
- Algorithm 3: A truncated 128-bit cryptographic MD5 (m = 128 bits, hash space K_3 = 2^{128} ≈ 3.40 × 10^{38}).
According to the Birthday Paradox, the probability P_{collision} of at least one hash collision among N items hashed into a uniform hash space of size K is approximated by:
P_{collision} ≈ 1 - e^{-(N^2) / (2K)}Calculate:
- The collision probability P_{collision} for Algorithm 1 (32-bit).
- The collision probability P_{collision} for Algorithm 2 (64-bit).
- The collision probability P_{collision} for Algorithm 3 (128-bit).
- Explain the catastrophic failure mode that occurs in conditional caching (
304 Not Modified) and optimistic concurrency (If-Match) when an ETag collision occurs.
Solution:
Step 1: Compute Collision Probability for Algorithm 1 (CRC32, K = 2^{32}):
N = 5 × 10^7N^2 = 2.5 × 10^{15}2K = 2 × 2^{32} ≈ 8.5899 × 10^9(N^2) / (2K) = (2.5 × 10^{15}) / (8.5899 × 10^9) ≈ 291,038P_{collision} ≈ 1 - e^{-291038} ≈ 1.0 (100% Certainty of Multiple Collisions)A 32-bit hash is completely unsuitable for 50 million items. In fact, a 50% collision probability for 32 bits occurs at merely N ≈ √{2^{32} × ln(2)} ≈ 77,163 items!
Step 2: Compute Collision Probability for Algorithm 2 (64-bit, K = 2^{64}):
2K = 2 × 2^{64} = 2^{65} ≈ 3.6893 × 10^{19}(N^2) / (2K) = (2.5 × 10^{15}) / (3.6893 × 10^{19}) ≈ 6.776 × 10^{-5}Using the Taylor expansion approximation 1 - e^{-x} ≈ x for small x:
P_{collision} ≈ 6.776 × 10^{-5} ≈ 0.00677\% (Approximately 1 in 14,750)While relatively small, a 0.0067% collision chance across 50 million commercial items means that several dozen catalog items will collide in production.
Step 3: Compute Collision Probability for Algorithm 3 (128-bit, K = 2^{128}):
2K = 2^{129} ≈ 6.8056 × 10^{38}(N^2) / (2K) = (2.5 × 10^{15}) / (6.8056 × 10^{38}) ≈ 3.67 × 10^{-24}P_{collision} ≈ 3.67 × 10^{-24}The probability is virtually zero, vastly smaller than the probability of uncorrectable hardware RAM bit-flips.
Step 4: Real-World Failure Modes of ETag Collisions:
- Cache Corruption (`304 Not Modified`): If Product A (a \50 book) and Product B (a \2,000 laptop) generate identical ETags, and an edge proxy or browser revalidates Product B with
If-None-Match: "collision_hash", the server returns304 Not Modified. The client displays Product A's cached description and pricing for Product B! - Optimistic Concurrency Failure (`If-Match`): If an item's state changes from
status: "in_stock"tostatus: "out_of_stock", but the new state produces a colliding ETag, anIf-Match: "collision_hash"update from another client succeeds when it should have been aborted with412 Precondition Failed.
Problem 3: Complete Optimistic Concurrency Engine in Node.js & PostgreSQL#
Construct a production PostgreSQL table schema and SQL update query utilizing an integer version column and RETURNING clause that executes atomic optimistic concurrency control without explicit database table-level locks (SELECT FOR UPDATE).
Solution:
-- schema.sql
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_number VARCHAR(32) UNIQUE NOT NULL,
balance_cents BIGINT NOT NULL CHECK (balance_cents >= 0),
version INT NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);// account-repository.ts
import { Pool } from 'pg';
export interface AccountRow {
id: string;
account_number: string;
balance_cents: number;
version: number;
updated_at: Date;
}
export class AccountRepository {
private db: Pool;
constructor(db: Pool) {
this.db = db;
}
// Executes an atomic optimistic update
public async updateAccountBalance(
accountId: string,
deltaCents: number,
expectedVersion: number
): Promise<{ success: boolean; updatedRow?: AccountRow; conflict: boolean }> {
// Single atomic statement checking expected version in the WHERE clause
const sql = `
UPDATE accounts
SET
balance_cents = balance_cents + 1,
version = version + 1,
updated_at = NOW()
WHERE id = 2 AND version = 3
RETURNING id, account_number, balance_cents, version, updated_at;
`;
const result = await this.db.query<AccountRow>(sql, [
deltaCents,
accountId,
expectedVersion
]);
// If rowCount === 1, the version matched and the update succeeded atomically
if (result.rowCount === 1) {
return { success: true, updatedRow: result.rows[0], conflict: false };
}
// If rowCount === 0, either the account doesn't exist or a version conflict occurred
const checkSql = `SELECT id FROM accounts WHERE id = 1;`;
const checkResult = await this.db.query(checkSql, [accountId]);
if (checkResult.rowCount === 0) {
return { success: false, conflict: false }; // Not Found
}
// Resource exists, but version did not match -> Conflict!
return { success: false, conflict: true };
}
}Through this pattern, the database engine enforces atomicity at the row lock level during the single UPDATE execution. If two concurrent transactions execute simultaneously with the same expectedVersion, one transaction succeeds and increments version to 2. The second transaction immediately encounters version = 1 as false, updating 0 rows and cleanly triggering an HTTP 412 Precondition Failed response without deadlock or thread blocking.
Problem 4: Stale-While-Revalidate Concurrency & Stampede Simulation#
A high-traffic e-commerce inventory API receives 2,000 requests/sec for a trending product.
Compare two cache architectures when the cache entry expires:
- Architecture 1 (Synchronous Expiration):
Uses Cache-Control: max-age=60. At t = 60 s, the cache expires completely.
- Architecture 2 (Asynchronous Stale-While-Revalidate with Lock):
Uses Cache-Control: max-age=60, stale-while-revalidate=30. At t = 60 s, incoming requests receive the stale data instantly while a single background worker acquires a distributed mutex (e.g., Redis SET lock NX EX 5) to refresh the cache.
The origin database query requires 150 ms to compute the complex pricing rules.
Calculate:
- The number of database queries triggered simultaneously at t = 60.0 s under Architecture 1 (Cache Stampede).
- The number of database queries triggered under Architecture 2.
- Contrast the client-perceived P99 latency during the revalidation window for both architectures.
Solution:
Step 1: Queries under Architecture 1 (Cache Stampede):
Incoming request arrival rate = 2,000 requests/sec = 2 requests/ms.
Database query duration = 150 ms.
During the 150 ms required for the first database query to finish and repopulate the cache, every single arriving request discovers an expired cache entry.
Total concurrent database queries dispatched:
Queries = 2,000 req/sec × 0.150 sec = 300 concurrent queries300 complex analytical queries hit the database simultaneously, exhausting the database connection pool, inducing CPU thrashing, and driving database query execution latency from 150 ms up to several seconds (cascading failure).
Step 2: Queries under Architecture 2:
Under stale-while-revalidate paired with a distributed mutex:
- Request 1 arrives at t = 60.001 s. It receives the stale cached payload in < 2 ms.
- Request 1 triggers background revalidation. The background thread attempts to acquire the distributed mutex
lock:product:42. It succeeds. - Requests 2 through 300 arrive between t = 60.001 s and t = 60.150 s. They all receive the stale cached representation in < 2 ms.
- Any secondary background threads attempting to acquire the mutex fail (lock already held) and exit quietly.
Total database queries dispatched: Exactly 1 query.
Step 3: Client Latency Comparison:
- Architecture 1: P99 latency escalates from 5 ms to > 2,500 ms during the stampede window.
- Architecture 2: P99 latency remains stable at < 10 ms throughout the entire transition. The user experiences zero latency spikes or service disruptions.