The page cache is a queue before it is durable storage
Trace buffered writes through dirty memory, allocation, writeback, and device completion while measuring sustained rather than cached throughput.
On this page
The question for today
What you’ll understand
- Distinguish visibility in memory from completion at a persistence boundary.
- Calculate dirty-data growth and drain time with explicit byte-rate assumptions.
- Design a throughput measurement that does not mistake cache absorption for sustained storage speed.
A buffered write can complete quickly because the kernel accepted bytes into memory, not because a storage device permanently recorded them. That buffering is useful: it combines writes, reuses hot data, and lets work overlap. It also creates a debt that must eventually be serviced. Treat dirty memory as queued work with ownership, errors, and a finite capacity, and several confusing storage performance graphs become easier to interpret.
One write has several completion boundaries
In the usual buffered path, an application supplies bytes and the kernel updates cached file contents, marking the affected memory dirty. Readers can observe these new contents through the filesystem's normal coherence behavior before those bytes are persistent. Later, writeback submits data toward storage and updates bookkeeping when operations finish. Filesystem allocation, metadata updates, block-layer handling, and device caching can all participate. The exact path depends on filesystem and I/O mode, so label each measurement by the boundary it observes instead of reporting one ambiguous write-completion time.
A successful write call returns a byte count, which may be smaller than requested. Handle partial progress and errors before assuming a complete record was accepted. A later writeback failure may surface at synchronization or another documented reporting point. Closing a descriptor or flushing a language-level buffer is not a substitute for the storage synchronization contract. Conversely, forcing every tiny update separately can destroy useful batching. Choose a commit boundary that matches the application promise, such as a durable checkpoint generation, and measure its cost explicitly.
Dirty memory can hide a slower steady state
Suppose a writer generates data faster than writeback can drain it. Initially the difference accumulates in available memory, and the application's observed rate can exceed persistent device throughput. Eventually memory policy, dirty limits, contention, or explicit synchronization constrains progress. The resulting drop is not automatically a defective device; it may be the transition from borrowing memory to paying the sustained cost. A benchmark must run long enough, or explicitly synchronize and account for draining, to expose the boundary its conclusion concerns.
When producers stop, dirty bytes still take time to drain. Data already read into a clean cache can make a later read benchmark measure memory rather than storage. Read and write caching therefore need separate treatment. Record the working-set size, I/O mode, cache preparation method, concurrency, and synchronization protocol with results. Bypassing the page cache does not by itself prove power-loss durability, because a device may still acknowledge writes into its own volatile cache. Performance and persistence boundaries remain separate even when one buffer is removed.
Observe both the producer and the debt
Watch admitted application bytes, dirty/writeback memory, completed device bytes, and end-to-end synchronization latency over the same time interval. Filesystem or device write amplification can make device bytes exceed application bytes; compressible data can also complicate comparisons. A falling dirty count does not alone prove the latest application transaction survived all required failure modes. Associate acknowledgments with checked synchronization results and keep delayed errors visible to the component that decides whether a model artifact may be advertised as ready.
The queue lab captures the simplest pressure balance. Treat one job as one chosen data unit. Press Reset, set both arrival to four and service to two, predict growth from the initial four-unit backlog, then Play or Step. For a separate drainage run, press Reset again, set both arrival to one and service to three, predict drainage from four, then Play or Step. A slider edit pauses but retains elapsed time and recalculates from the initial four; it does not switch a live accumulated backlog to new rates. The separate M/M/1 readout assumes Poisson arrivals, exponential service, and a stable single server, not Linux writeback.
Work it through
Account for a fast-looking burst
Assume an initially empty 3 GB dirty-data budget, application admission of 800 MB/s, and sustained drain of 500 MB/s. Ignore throttling until the budget fills; use decimal units.
Compute net dirty growth: 800 − 500 = 300 MB/s. The 3,000 MB budget fills after 3,000/300 = 10 seconds.
During those ten seconds, the application admits 8,000 MB while storage drains 5,000 MB. The 3,000 MB difference remains as dirty work.
If the application then stops, draining 3,000 MB at 500 MB/s takes six additional seconds. Total admitted bytes divided by the full sixteen seconds gives 500 MB/s.
The 800 MB/s burst borrowed cache capacity. Under the stated constant-rate model, the full write-and-drain interval reveals the 500 MB/s sustained boundary.
The living notebook
Explore the model
Interactive lab · queue
Watch admitted work outrun writeback
Press Reset, set both λ=4 and μ=2 for equal-sized dirty-data chunks, predict growth from four units, then Play or Step. For a separate drainage comparison, Reset, set both λ=1 and μ=3, predict, then Play or Step. Reset restores defaults, so re-enter both rates. This fluid model omits kernel throttling, device caches, and writeback errors.
- Fluid backlog
- 4
- Net rate · λ − μ
- −1 /s
- M/M/1 mean residence
- 1 s
Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forBuffered admission can temporarily exceed persistent throughput by accumulating work that still needs to drain.
Pause & reason
Check your understanding
Your turn
Try it yourself
A writer admits 120 MB/s while storage drains 80 MB/s for fifteen seconds from an empty backlog. It then stops. Estimate the backlog and additional drain time under constant rates.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Accumulate the difference in rates, then divide the remaining bytes by the drain rate.
Compare with the solution
The dirty backlog grows by 40 MB/s for fifteen seconds, giving 600 MB. With no new admission, 600/80 = 7.5 seconds are needed to drain. This model ignores throttling, metadata work, changing bandwidth, and device-cache persistence, which must be measured separately.
Keep the idea
Take it with you
- Buffered visibility, writeback completion, and durable application acknowledgment are distinct milestones.
- A short benchmark can consume cache capacity while overstating sustained throughput.
- Measure outstanding work and delayed errors alongside application byte rates.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.