Write-Around and Write-Behind Caching: Optimizing Write-Heavy Workloads
In this tutorial, you will learn about Write. We cover key concepts, practical examples, and best practices to help you master this topic.
Write-around Caching bypasses the cache on write operations — data is written directly to the database, and the cache is populated only on subsequent reads (cache-aside). Write-behind caching (also called write-back) asynchronously batches writes to the database, offering lower write latency at the risk of data loss on cache failure.
flowchart TB
subgraph Write-Around
WA1[Client Write] --> WA2[Database]
WA1 --> WA3[Cache - Bypassed]
WA4[Client Read] --> WA5{Cache Hit?}
WA5 -->|Miss| WA6[Read from DB, Update Cache]
WA5 -->|Hit| WA7[Return Cached]
end
subgraph Write-Behind
WB1[Client Write] --> WB2[Cache - Fast Ack]
WB2 --> WB3[Async Queue]
WB3 --> WB4[Batch Write to DB]
end
What You'll Learn
- Write-around: avoiding cache pollution from write-heavy workloads
- Write-behind: buffering writes for throughput and batching
- Trade-offs: consistency, durability, and latency
- Combining strategies for different data access patterns
Why It Matters
Write-around prevents cache pollution from one-time writes that are never read again. Write-behind reduces write latency by 10-100x by batching and deferring database writes, but introduces a window of potential data loss.
Real-World Use
A logging service writes 10,000 log entries per second. Using write-behind, logs are buffered in Redis and flushed to PostgreSQL in batches of 500 every second. This reduces write latency from 20ms to 0.5ms and reduces database write load by 500x.
Write-Around Implementation
async function writeAround(table, data) {
const result = await db.query(`INSERT INTO ${table} SET ?`, [data]);
// Cache is NOT updated. Next read will populate it via cache-aside.
return result.insertId;
}
async function readWithCacheAside(id) {
const cacheKey = `post:${id}`;
const cached = await cache.get(cacheKey);
if (cached) return JSON.parse(cached);
const [rows] = await db.query('SELECT * FROM posts WHERE id = ?', [id]);
if (rows.length > 0) {
await cache.setEx(cacheKey, 3600, JSON.stringify(rows[0]));
}
return rows[0];
}
Expected output:
Write: data goes to DB only, no cache overhead. Read on a new ID: fetches from DB, caches result. Subsequent reads: cache hit.
Write-Behind with In-Memory Buffer
class WriteBehindBuffer {
constructor(flushInterval = 1000, batchSize = 100) {
this.buffer = [];
this.flushInterval = flushInterval;
this.batchSize = batchSize;
this.timer = setInterval(() => this.flush(), flushInterval);
}
async write(key, data) {
this.buffer.push({ key, data, timestamp: Date.now() });
if (this.buffer.length >= this.batchSize) {
await this.flush();
}
return { queued: true };
}
async flush() {
if (this.buffer.length === 0) return;
const batch = this.buffer.splice(0, this.batchSize);
try {
const values = batch.map(item => [item.data]);
await db.query('INSERT INTO logs (data) VALUES ?', [values]);
console.log(`Flushed ${batch.length} entries`);
} catch (err) {
console.error('Flush failed, re-queuing batch:', err.message);
this.buffer.unshift(...batch);
}
}
shutdown() {
clearInterval(this.timer);
return this.flush();
}
}
Expected output:
Writes return instantly (<1ms). Buffer flushes every 1s or 100 entries. On failure, batch is re-queued. On shutdown, remaining buffer is flushed.
Write-Behind with Redis Queue
const { Queue } = require('bullmq');
const writeQueue = new Queue('db-writes', {
connection: { host: 'redis-queue', port: 6379 },
defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 1000 } }
});
async function queueWrite(table, data) {
await writeQueue.add('write', { table, data, timestamp: Date.now() });
return { queued: true, id: data.id };
}
// Worker process
const { Worker } = require('bullmq');
const worker = new Worker('db-writes', async (job) => {
const { table, data } = job.data;
await db.query(`INSERT INTO ${table} SET ?`, [data]);
}, { concurrency: 5 });
Expected output:
Write returns immediately. Redis queue persists jobs. Worker processes 5 concurrent writes, retries on failure with exponential backoff.
Common Mistakes
- Using write-around for data that is read immediately after write (e.g., user settings page) — the subsequent read will miss the cache.
- Using write-behind for critical financial data where durability is more important than latency.
- Not flushing the write-behind buffer before the application shuts down, causing data loss.
- Setting the batch size too large, causing long DB queries that block other operations.
- Not monitoring write-behind queue depth — a growing queue indicates the database cannot keep up.
Practice Questions
- How does write-around differ from write-through?
- When is write-around the best choice?
- What durability risks does write-behind introduce?
- How does batch size affect write-behind performance?
- What happens to buffered writes if the application crashes?
Challenge
Design a caching Strategy for a photo upload service. Photo metadata is read frequently; photo files are large and read rarely after upload. Choose write strategy for each. Implement write-behind with Redis for the metadata and write-around for the photo files.
FAQ
Mini Project
Build a logging service with two modes: write-around (logs go directly to DB) and write-behind (logs buffer in a Redis queue and flush to DB every 500ms). Compare write latency and throughput under a 1000 requests/second load. Measure the data loss window for write-behind.
What's Next
Continue with Write-Back Caching for a detailed comparison with write-behind and dirty-page management.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro