Volume I · Part II · Chapter 5
Knowing Your Workloads
Give me a report showing that a machine uses a fifth of its CPU and I can make a plausible argument for shrinking it. Tell me that the machine runs a critical database and I can make an equally plausible argument for leaving it alone. Neither argument requires much knowledge of the database, which is precisely what should make us uncomfortable.
The interesting work begins when we try to explain why that fifth of the CPU is all the system uses. Perhaps nobody needs the machine anymore. Perhaps the application is waiting on a lock, or the memory is keeping an expensive set of reads away from storage. Perhaps the person who called it critical knows something about next month’s work that has never appeared in our measurements. I want to understand those possibilities before we turn either the low utilization or the word critical into an answer.
That means asking what the system does, what stops it from doing more, and who can tell us what the business will need from it next.
How to read this chapter
This chapter is about characterizing work before sizing supply: what the machines are being asked to do, what drives that work, and what makes that description fit to use in a decision.
- Begin with the work, not the machine — what a request contains, who sends it, and how that is changing.
- Use simple arithmetic where it is useful — the utilization law, and how far a changed mix moves the answer.
- Look at utilization, waiting, and errors — the USE method, locks and imposed limits, and cache sensitivity.
- Test the arrivals you expect to receive — open and closed load, coordinated omission, and Little’s law.
- When scale changes the system — the Universal Scalability Law and the limits of a fitted curve.
- The apparently idle resource — deciding before the evidence is complete.
- A record someone can challenge — an evidence record a specialist can dispute line by line.
The lesson to carry: a measurement earns its meaning from the work it describes and the conditions of the test, not from the machine it was taken on.
Begin with the work, not the machine
Our constructed portfolio starts from 250 completed requests per second per worker, with p95 latency no greater than 200 milliseconds and errors no more than 0.1 percent of offered requests: a synthetic fixture, assumed to come from representative tests under a particular request mix.
Consider everything hidden inside “a particular request mix.” Fetching a cached description, updating an account, searching years of history, and initiating work in half a dozen other services can all appear in a report as requests, so a stable request count can conceal a considerable change in the work. Before asking how many requests we expect next month, I want to know whether they will resemble the ones we have measured.
Brendan Gregg summarizes workload characterization as four questions: who is causing the load, why it is being called, what it consists of, and how it is changing over time.1 For capacity planning, the first and last are business questions as much as technical ones. Telemetry knows who sent last month’s requests; Product may know that next month’s will come from different customers.
Suppose the staged rollout reaches customers with much larger histories than those in the pilot. Product’s estimate of 2,000 additional requests per second and Engineering’s successful test may both be accurate, and we still need a test containing the new customers’ operations and data. A safety factor on the old result would produce another number without teaching us anything about the change.
The why matters too. Two deployments of the same messaging software can carry similar ordinary traffic while one sends routine notifications and the other a holiday campaign whose messages are worthless a day late. I would rather ask the business owner which message becomes useless if it arrives tomorrow than spend another week measuring an interval in which nobody needed that answer.
Use simple arithmetic where it is useful
Some questions get a useful first answer from very little machinery. Take a hypothetical, deliberately simple workload: stateless, CPU-bound tasks on interchangeable cores, with a tested demand of 0.02 CPU-seconds per task, measured as CPU-seconds consumed divided by tasks completed over the same interval. The utilization law of operational analysis says that utilization, counted here in busy cores, equals throughput multiplied by that demand, .2 At 1,000 tasks per second, average CPU demand is:
At a planning utilization of 60 percent, the first estimate is 20 / 0.60 = 33.33, rounded up to 34 cores. Under textbook queueing assumptions, one server that is 60 percent busy makes the average task wait 0.6 / (1 − 0.6) = 1.5 service times; our 34 cores, sharing one queue and about as busy, leave fewer than one task in 300 waiting at all.3 A percentage that is cautious for a pool already means real waiting for a single lock, disk, or partition consumer.
The same arithmetic shows how far the web service’s request mix can move before Chapter 4’s plan fails. With the borrowed batch workers, the service keeps 13,500 requests per second after losing a failure group, against a staged estimate of 12,000; since 13,500 / 12,000 = 1.125, the margin disappears if the average request needs 12.5 percent more of whatever limits a worker. Suppose history searches are 5 percent of the tested mix at 22 milliseconds of CPU each, against 2 for everything else: an average of 0.95 × 2 + 0.05 × 22 = 3.0 milliseconds. If the first stage’s customers push searches to 7 percent, the average becomes 0.93 × 2 + 0.07 × 22 = 3.4. A CPU-limited worker would then qualify at about 250 × 3.0 / 3.4 ≈ 221 requests per second, probably less once longer searches stretch the latency tail, and 54 of them carry about 11,900.
Arithmetic is most valuable when its answer is lopsided, far above the proposed allocation or comfortably inside it. When a plausible change is bigger than the margin, as a two-point shift in searches is here, its job is to name the test to run before the launch. My objection comes when a preliminary estimate survives into a presentation with all the confidence of a result and none of the conditions that made it provisional: that the work parallelizes, that a shared database keeps up, that a queue spreads the work evenly.
Look at utilization, waiting, and errors
Gregg’s USE method—utilization, saturation, and errors—gives resource investigation a disciplined start: for every resource, check all three. He defines utilization as the average time a resource was busy servicing work, saturation as the degree to which it has extra work it can’t service, often queued, and errors as the count of error events.4 The averaging matters. Gregg describes a customer whose CPUs were saturated although monitoring, reporting five-minute averages, never showed them above 80 percent; they were hitting 100 percent for seconds at a time. Relating busy, queued, and failing work tells us far more than sorting a spreadsheet of CPU percentages from smallest to largest.
The method extends to software resources and imposed limits: a lock’s utilization is the time it was held, its saturation the threads queued for it. In a cloud account, the limit that saturates is often an allowance, such as a container’s CPU limit or a disk’s provisioned IOPS, which is one way a machine can look idle while its workload is throttled.5
For a proposed change, follow the customer’s work alongside the resource measurements. If completed work stays flat while the queue grows, where is that work waiting? If latency rises while CPU remains low, what consumed the elapsed time? An error rate can even improve because more work is turned away before it reaches the boundary we measure. Those questions need the application engineer and the database or platform specialist in the same conversation.
PostgreSQL exposes this kind of saturation directly. Each session in pg_stat_activity reports the event it is waiting for, if any: I/O, a heavyweight or lightweight lock, a buffer pin, or the client, among others.6 Sampled every second or so through a busy period, this query shows where active sessions spend their time:
SELECT wait_event_type, wait_event, count(*) AS sessions
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND state = 'active' AND pid <> pg_backend_pid()
GROUP BY wait_event_type, wait_event
ORDER BY sessions DESC;
A null wait type usually means the session is on a CPU or queued in the operating system for one. If most sessions wait on Lock, another core will not shorten the wait. We need the transaction holding the lock, which Chapter 12 follows into schema design and application behavior, a long way from the smaller machine the meeting set out to buy.
I use data-bound workload for work whose limiting behavior depends materially on data access, movement, layout, state, or coordination. Disk-bound would be too narrow, as a small cache example shows. At 10,000 logical page requests per second and a 99 percent hit rate, assume each miss requires one physical read and hits require none. Physical read demand is:
Suppose reducing memory moves the hit rate to 95 percent. The new demand is 10,000 × (1 − 0.95) = 500 reads per second: five times as much physical I/O from a four-percentage-point change. If our storage path can sustain 300 reads per second at the required quality, an apparently modest adjustment has crossed the service’s limit.
These are constructed numbers; a real investigation has to establish the cache behavior and the storage limit, and PostgreSQL adds a trap. Its blks_hit counter includes only hits in its own buffer cache, and its I/O statistics cannot tell a kernel page-cache hit from a read that reached storage. The documentation suggests starting shared_buffers at a quarter of a dedicated server’s memory and doubts that more than 40 percent helps, because PostgreSQL also relies on the operating system’s cache.7 Shrink the machine, and some of the cache we remove may be one the database’s hit ratio never counted.
Test the arrivals you expect to receive
The workload generator deserves the same skepticism as the system under test. A reassuring latency result means little if the test quietly stopped sending work whenever the service became slow, and many generators do exactly that by default: each simulated client waits for a response before sending its next request.
Gil Tene calls the resulting measurement error coordinated omission: the generator stops sending the requests that would have suffered most.8 In his wrk2 documentation, a 30-second run aimed at 2,000 requests per second over 100 connections included one 1.4-second server pause. Measured from when each request should have been sent, the 99th percentile was 1.27 seconds; measured from when each was actually sent, 6.04 milliseconds. About 2,800 requests were due during the pause, 7 percent of the 39,500 recorded, but 100 waiting connections could record at most 100 slow responses, too few to reach the 99th percentile.
Whether waiting is a flaw depends on the work. In a closed model, a fixed population of clients each waits for a response, and perhaps a pause, before sending again; in an open model, work arrives on its own schedule. Schroeder, Wierman, and Harchol-Balter found a vast difference in behavior between the two.9 An employee working through a form is roughly closed; devices reporting on a timer and customers arriving from a campaign do not slow down because the service has. Either way, offered, started, completed, rejected, timed-out, and skipped work need to stay distinguishable, or the workload can appear to improve just as the experiment becomes less demanding.
Azure Load Testing runs JMeter and Locust scripts at scale, and both tools are closed by default. Microsoft’s sizing guidance gives the rate a test can generate as virtual users divided by latency: 2,000 users produce about 100,000 requests per second at 20 milliseconds, and only 10,000 at 200.10 That is Little’s law: the average number of requests in a system equals throughput multiplied by the average time each spends there, here latency plus any scripted think time.11 It is also a quick check on any test report, whose concurrency, throughput, and latency have to reconcile. For a launch question stated as an arrival rate, I would use an arrival-rate design where the tool has one and add a fail criterion on requests_per_sec beside those on p95(response_time_ms) and percentage(error), so that a run that fell short of the intended rate fails instead of passing quietly.
For our web service, the report needs the latency distribution, the rate actually offered, and the successful completions together, with the full histograms kept. HdrHistogram records distributions that can be added together; percentiles cannot, and two runs’ p95s do not average to the p95 of both.12 Once all that survives is a p95 on a slide, we can no longer combine results or investigate an unexpected interval.
When scale changes the system
The portfolio’s capacity arithmetic multiplies workers by 250, which Chapter 2 justified by treating every configuration as having passed the same test at its full size. Neil Gunther’s Universal Scalability Law describes what that leaves out. It gives the relative capacity of workers, their throughput divided by that of one, as
where measures contention, waiting for something shared, and measures coherency, the cost of keeping copies of shared state consistent.13 With both at zero, the law is our multiplication. Small coefficients are not small in effect: a contention coefficient of 0.01 gives 54 workers the capacity of 54 / 1.53 ≈ 35, and no number of workers can reach times the throughput of one. Any coherency cost produces a peak at , beyond which more workers deliver less; with and , it comes at 70 workers.
Gunther is careful about what the model can do. He asks for at least half a dozen measured load points, warns that the law “is not a crystal ball” and cannot foretell broken measurements or intrinsic pathologies, and calls its application to production data with mixed workloads a current area of research. Those cautions are where my concern begins. Coefficients fitted to one schema, data distribution, and request mix describe that workload. Why should they hold after the cache is exhausted, a partition becomes unusually busy, a rebuild competes with customer traffic, or the rollout shifts the mix toward searches that work the shared database harder? Holding back measurements to test a model’s predictions is useful; so is writing down which changes would make us stop using it.
For the web service, 13,500 is therefore a statement about the tested range and the tested mix, and the batch machines still have to become operationally equivalent within the two-day preparation window. I would accept the multiplication once tests at the new mix show per-worker throughput holding across the configurations we intend to use, with the shared database measured at the rate we intend to promise. Until then we have a promising option to qualify, and a tipping point to look for: the relationship that stops holding and the mechanism that changed it.
The apparently idle resource
Now we reach the situation that makes this work uncomfortable: we cannot complete the investigation before the decision is due, and somebody has already counted the apparently idle resource toward a savings target. The Cassandra cluster in the Introduction, at 1 to 2 percent CPU, is the expensive version of that mistake.
Consider a constructed data service with an infrequent, urgent rebuild. The operating team knows it can require a large allocation, but a recent data change has left them without a representative measurement. Ordinary utilization is low, the next consequential event is near, and although the allocation could eventually be restored after removal, nobody has established that it could be restored in time.
Two questions decide how much evidence the decision needs. Could we undo it faster than we would need to? If quota, supply, preparation, and qualification can restore the allocation within the warning we would get before the next rebuild, releasing it is an experiment we can watch and reverse; if not, it is one-way and should wait for evidence about the event it exposes. And could the missing evidence plausibly change the answer, as it could for the web service’s margin? Here neither answer favors releasing the allocation now.
I would preserve it for a bounded period and fund the test, with the rebuild owner explaining the obligation we are protecting. The decision needs to carry the cost of holding the resource, what remains unknown, and the evidence or date at which we will reconsider it. Useful work may be able to run there meanwhile, under an agreement to return the capacity when the rebuild needs it.
What I would resist is letting the exception become permanent because everybody agrees the technology is difficult. Somebody has to sponsor the investigation and own the retained capacity, or the issue needs escalation rather than another quiet extension.
Nor can we escape the judgment by inventing a probability. Suppose an optimization saves $2,000 a month, or $24,000 a year, and a relevant failure would cost $400,000. In a simple model with at most one such loss per year, an added annual probability of 0.06 would give 0.06 × $400,000 = $24,000 in expected annual loss. Nobody can tell us whether six percent is right or whether $400,000 captures the consequence, but the break-even turns an unanswerable question into one the rebuild owner can answer: would removing the allocation add more than about a one-in-seventeen chance per year of that failure?
The person responsible for the business may reject that exposure even when expected cost appears favorable; FinOps usage-optimization guidance likewise asks for value-versus-risk thresholds for each workload.14 The proposal should make that exposure specific enough that the person deciding knows what they are agreeing to carry.
A record someone can challenge
By now we can keep three kinds of knowledge apart instead of blending them into one confidence score: measurements from a known interval and configuration, experimental claims bounded by what was tested, and business judgments about demand, deadlines, and acceptable consequences. For the web service, a short evidence record might read:
| Claim | Evidence | What would change it |
|---|---|---|
| 250 requests/second per worker at p95 ≤ 200 ms, errors ≤ 0.1% | Experiment: tested mix, each configuration used | A different mix; a generator that could not hold its rate |
| Next peak of 10,000 requests/second | Measurement and forecast: 16 weeks of daily one-minute peaks of offered requests | Peak demand rejected or queued; sub-minute bursts |
| Staging adds 2,000 requests/second | Business judgment: product lead, dated, staged scope only | Changed scope or customers; the first stage’s peak |
| 13,500 requests/second after losing a group | Arithmetic: 54 × 250, linear within tested configurations | Per-request demand on the limit rising over 12.5% |
| The shared database is not the limit at 13,500 | None yet | A database test at the new mix, before launch |
A missing metric must not become zero during an export, and a successful experiment on one workload must not silently become permission to shrink every machine of the same type. Contrary runs and failed tests belong in the record too. It is working if a specialist or customer can point to the line they think we have misunderstood.
Another month of ordinary CPU averages would not change a line of that record. Having established what we need to know, we can choose the tools that will help us find it. That is a much better order than acquiring a dashboard and then trying to make the business fit the measurements it happens to display.
Notes and sources
Brendan Gregg, Performance Analysis Methodology, https://www.brendangregg.com/methodology.html, accessed October 4, 2026. Gregg notes that the general process predates his 2013 write-up, pointing to Raj Jain, The Art of Computer Systems Performance Analysis (1991). Attaching business-change intelligence is this book’s emphasis.↩︎
Peter J. Denning and Jeffrey P. Buzen, “The Operational Analysis of Queueing Network Models,” ACM Computing Surveys 10, no. 3 (1978): 225–261, https://doi.org/10.1145/356733.356735; Edward D. Lazowska et al., Quantitative System Performance (1984), chapter 3, https://homes.cs.washington.edu/~lazowska/qsp/, accessed October 3, 2026. The law holds for measured averages without distributional assumptions; it says nothing about latency.↩︎
Mor Harchol-Balter, Performance Modeling and Design of Computer Systems (Cambridge University Press, 2013), chapters 13–14, https://www.cs.cmu.edu/~harchol/PerformanceModeling/book.html, accessed October 3, 2026. M/M/1 and M/M/k results; the 34-core figure, about 0.29 percent, was computed for this chapter with the Erlang C formula. Real arrivals are often burstier.↩︎
Brendan Gregg, The USE Method, https://www.brendangregg.com/usemethod.html, accessed October 4, 2026. Gregg also notes a capacity-based definition of utilization; the method sets no universal utilization target.↩︎
Gregg, The USE Method, https://www.brendangregg.com/usemethod.html, accessed October 4, 2026, sections on software resources (including locks) and cloud computing (hypervisor and container limits). The provider examples are the author’s.↩︎
PostgreSQL 18 documentation, “The Cumulative Statistics System,” https://www.postgresql.org/docs/18/monitoring-stats.html, accessed October 4, 2026. The query ran on a disposable PostgreSQL 14 instance, using columns documented for version 18; it has not been run on PostgreSQL 18 or a production database.↩︎
PostgreSQL 18 documentation, “The Cumulative Statistics System” (
blks_hit, kernel page cache) and “Resource Consumption,” https://www.postgresql.org/docs/18/runtime-config-resource.html (shared_buffers), accessed October 4, 2026. The cache arithmetic is a constructed model, not benchmark data.↩︎Gil Tene, “How Not to Measure Latency,” QCon San Francisco 2012, https://qconsf.com/sf2012/dl/qcon-sanfran-2012/slides/GilTene_HowNotToMeasureLatency.pdf, accessed October 3, 2026;
wrk2README, https://github.com/giltene/wrk2, accessed October 4, 2026. The percentiles and the 39,500 requests recorded after a 10-second calibration period are from the README’s second example; the other request counts are this chapter’s arithmetic.wrk2is an HTTP tool.↩︎Bianca Schroeder, Adam Wierman, and Mor Harchol-Balter, “Open Versus Closed: A Cautionary Tale,” NSDI 2006, https://www.usenix.org/conference/nsdi-06/open-versus-closed-cautionary-tale, accessed October 4, 2026. The workload examples are the author’s.↩︎
Microsoft Learn, “What is Azure Load Testing?”, “Configure Azure Load Testing for high-scale load”, and “Load test configuration YAML”, checked October 4, 2026: supported tools, the RPS formula and 2,000-user example, and fail criteria. JMeter’s Open Model Thread Group is marked experimental; Locust’s wait time “can only constrain the throughput.” The 200-millisecond case is this chapter’s arithmetic. No Azure load test was run for this book.↩︎
John D. C. Little, “A Proof for the Queuing Formula: ,” Operations Research 9, no. 3 (1961): 383–387, https://doi.org/10.1287/opre.9.3.383. Quantitative System Performance, chapter 3, adds think time and recommends the law for checking the consistency of measurements. It relates averages, not percentiles.↩︎
HdrHistogram README, https://github.com/HdrHistogram/HdrHistogram, accessed October 4, 2026. Histograms are additive and can correct for coordinated omission when the intended interval is known; neither shows the workload was representative.↩︎
Neil J. Gunther, “Universal Scalability Law,” https://www.perfdynamics.com/Manifesto/USLscalability.html, accessed October 4, 2026. Formula, ceiling, peak, and the cautions quoted; first published in 1993 as the “super serial” model. The numerical examples and limits on extrapolation are the author’s.↩︎
FinOps Foundation, “Usage Optimization,” https://www.finops.org/framework/capabilities/usage-optimization/, accessed October 4, 2026. The guidance also warns about longer-cycle peaks such as month-end; it does not set thresholds for any particular workload.↩︎