Write-Back Caching: Deferred Writes with Dirty Page Management
In this tutorial, you will learn about Write. We cover key concepts, practical examples, and best practices to help you master this topic.
Write-back Caching (also called write-behind) acknowledges writes immediately after updating the cache, then asynchronously persists the changes to the database. The cache tracks which entries are "dirty" (modified but not yet persisted) and flushes them in batches for optimal throughput.
flowchart TB
Client -->|Write Request| App
App -->|1. Write to Cache| Cache[(Cache)]
Cache -->|2. Mark Dirty| DirtyMap
App -->|3. Ack Client| Client
Cache -->|4. Async Flush Scheduler| Scheduler
Scheduler -->|5. Batch Flush| DB[(Database)]
DB -->|6. Mark Clean| DirtyMap
Scheduler -->|7. Retry on Failure| Retry[Retry Queue]
What You'll Learn
- Dirty page tracking and write-back buffer management
- Flush policies: time-based, count-based, threshold-based
- Crash recovery: replaying dirty pages on restart
- Trade-offs between write-back and write-through
Why It Matters
Write-back caching achieves the lowest write latency by decoupling the client-facing acknowledgment from the database write. It can increase write throughput by 50x while reducing database connection contention — ideal for logging, analytics, and metrics ingestion.
Real-World Use
An IoT platform ingests 100,000 sensor readings per second. Each reading is written to Redis with a dirty flag. Every 2 seconds, a background worker flushes all dirty readings to Cassandra in batches of 1000. Write latency is under 1ms for 99% of requests.
Write-Back Cache Implementation
Dirty Page Tracker
class DirtyPageTracker {
constructor() {
this.dirty = new Map();
this.flushInProgress = false;
}
markDirty(key, value) {
this.dirty.set(key, { value, timestamp: Date.now() });
}
markClean(key) {
this.dirty.delete(key);
}
getDirtyPages() {
return Array.from(this.dirty.entries()).map(([key, entry]) => ({
key, value: entry.value, timestamp: entry.timestamp
}));
}
isDirty(key) {
return this.dirty.has(key);
}
size() {
return this.dirty.size;
}
}
Expected output:
Dirty pages are tracked by key. On flush, they are read, sent to DB, and marked clean. Crash recovery iterates remaining dirty entries on startup.
Write-Back Cache with Periodic Flush
class WriteBackCache {
constructor(flushIntervalMs = 2000, maxDirty = 1000) {
this.cache = new Map();
this.dirtyTracker = new DirtyPageTracker();
this.flushIntervalMs = flushIntervalMs;
this.maxDirty = maxDirty;
this.timer = setInterval(() => this.flush(), flushIntervalMs);
}
async get(key) {
return this.cache.get(key)?.value ?? null;
}
async set(key, value, dbTable) {
this.cache.set(key, { value, dbTable });
this.dirtyTracker.markDirty(key, value);
if (this.dirtyTracker.size() >= this.maxDirty) {
await this.flush();
}
return { written: true };
}
async flush() {
if (this.dirtyTracker.size() === 0 || this.dirtyTracker.flushInProgress) return;
this.dirtyTracker.flushInProgress = true;
const dirtyPages = this.dirtyTracker.getDirtyPages();
const groups = this.groupByTable(dirtyPages);
for (const [table, entries] of Object.entries(groups)) {
try {
const values = entries.map(e => e.value);
await db.query(`INSERT INTO ${table} (data) VALUES ?`, [values]);
entries.forEach(e => this.dirtyTracker.markClean(e.key));
} catch (err) {
console.error(`Flush failed for ${table}:`, err.message);
}
}
this.dirtyTracker.flushInProgress = false;
}
groupByTable(pages) {
const groups = {};
for (const page of pages) {
const entry = this.cache.get(page.key);
const table = entry?.dbTable || 'default';
if (!groups[table]) groups[table] = [];
groups[table].push(page);
}
return groups;
}
async recover() {
const dirtyPages = this.dirtyTracker.getDirtyPages();
if (dirtyPages.length > 0) {
console.log(`Recovering ${dirtyPages.length} dirty pages...`);
await this.flush();
}
}
shutdown() {
clearInterval(this.timer);
return this.flush();
}
}
Expected output:
Writes acknowledged in <1ms. Flush interval: 2s or 1000 dirty entries. On shutdown or recovery, remaining dirty pages are flushed.
Write-Back with Shutdown Hook
const writeBackCache = new WriteBackCache(2000, 500);
process.on('SIGINT', async () => {
console.log('Shutting down write-back cache...');
await writeBackCache.shutdown();
console.log('All dirty pages flushed.');
process.exit(0);
});
process.on('SIGTERM', async () => {
await writeBackCache.shutdown();
process.exit(0);
});
Expected output:
On graceful shutdown, the write-back cache flushes all remaining dirty pages before exiting. This prevents data loss during deployments.
Common Mistakes
- Not tracking dirty pages separately from the cache — after a flush, the cache still has the data but it should be marked clean.
- Using a single flush interval for all table types — high-priority data should flush more frequently.
- Not implementing backpressure — if the database cannot keep up, dirty pages accumulate and the cache grows unbounded.
- Ignoring flush failures — if a batch fails, the dirty pages remain dirty but the application may not retry.
- Not logging dirty page count — a growing backlog is the first symptom of a write-back bottleneck.
Practice Questions
- In write-back caching, what makes a cache entry dirty?
- How does write-back differ from write-through in write latency?
- What happens to dirty pages if the application crashes?
- Why is batch flushing more efficient than flushing entries one-by-one?
- How do you implement backpressure in a write-back cache?
Challenge
Design a write-back cache for a time-series metrics service that receives 50,000 data points per second. Implement dirty page tracking, batch flushing to InfluxDB every 3 seconds, crash recovery with Redis persistence, and a health endpoint that reports current backlog.
FAQ
Mini Project
Extend the logging service from the write-around lesson. Implement write-back with dirty page tracking. Add a /status endpoint showing: total dirty pages, last flush time, flush success count, and error count. Write a crash simulation test that verifies recovery.
What's Next
Continue with Cache-Aside Pattern to revisit the most common caching pattern in depth.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro