Skip to content

AI-Assisted Capacity Planning from sar and vmstat Data

Turn weeks of sysstat history into a capacity forecast: reduce sar data to daily percentiles, fit the trend yourself, and let an LLM interpret headroom and risks.

●By 11 min read

Most Linux servers already record the data you need for capacity planning. If sysstat is installed, a sample of CPU, memory, run queue, disk and network counters lands on disk every ten minutes. Almost nobody looks at it until something is slow, and by then the question is no longer "when will we run out" but "why did we".

An LLM is a useful second reader for this data, with one hard rule: it does not do the arithmetic. Models are unreliable at computing percentiles or regressions over thousands of rows. You reduce the raw samples to daily percentiles and fit the trend with deterministic tools, then hand the model a compact table and ask it to interpret: which resource saturates first, which numbers are misleading, and what would change the forecast.

Collect Enough History

A forecast from one week of data is a guess. You want at least four weeks, ideally a full quarter, so that month-end jobs and weekly cycles show up. Install and enable sysstat, then check how long it keeps data:

# Debian / Ubuntu

$ sudo apt install sysstat

$ sudo systemctl enable --now sysstat

# RHEL / Fedora

$ sudo dnf install sysstat

$ sudo systemctl enable --now sysstat

# Confirm collection is running and see today's file

$ systemctl list-timers 'sysstat*'

$ sar -u | tail -3

Debian / UbuntuRHEL / Fedora
Data files/var/log/sysstat/saDD/var/log/sa/saDD
Config/etc/sysstat/sysstat/etc/sysconfig/sysstat
Default HISTORY7 days28 days
Collectionsysstat-collect.timer (cron on older releases)sysstat-collect.timer

Raise HISTORY on Debian β€” seven days is too short to see a trend. Set HISTORY=28 or more in /etc/sysstat/sysstat. Older Debian and Ubuntu releases also need ENABLED="true" in /etc/default/sysstat.

Ship daily summaries off the box β€” the binary sa files are tied to the sysstat version that wrote them and roll over by day of month. For quarters of history, export a daily summary (next section) to central storage and keep that instead.

Ten-minute averages hide spikes β€” a 30-second CPU saturation every hour disappears into a 600-second average. That is why the peak window gets vmstat at a finer interval, covered below.

Reduce Raw Samples to Daily Percentiles

sadf -d exports sar data as semicolon-separated text with one header line per report. Column positions differ between sysstat versions, so read the header before writing any awk:

$ sadf -d /var/log/sa/sa28 -- -u | head -1 | tr ';' '\n' | nl

1 # hostname

2 interval

3 timestamp

4 CPU

5 %user

6 %nice

7 %system

8 %iowait

9 %steal

10 %idle

$ sadf -d /var/log/sa/sa28 -- -u | sed -n 2p

web01;600;2026-09-28 00:10:01 UTC;-1;12.31;0.00;3.12;0.41;0.00;84.16

CPU -1 means all CPUs combined. Busy time is 100 - %idle. This pulls every retained day into one file of DATE VALUE pairs, then a small awk script turns each day into p50, p95 and max:

$ for f in /var/log/sa/sa[0-9][0-9]; do sadf -d "$f" -- -u; done |

grep -v '^#' |

awk -F';' '{ split($3, t, " "); print t[1], 100 - $10 }' |

sort -k1,1 -k2,2n > cpu_busy.txt

# daily.awk: sorted "DATE VALUE" lines in, one summary line per day out

function flush( i) {

if (!n) return

i = int(n * 0.95); if (i < 1) i = 1

printf "%s p50=%.1f p95=%.1f max=%.1f\n", d, v[int((n + 1) / 2)], v[i], v[n]

}

$1 != d { flush(); d = $1; n = 0 }

{ v[++n] = $2 }

END { flush() }

$ awk -f daily.awk cpu_busy.txt > cpu_daily.txt

$ tail -3 cpu_daily.txt

2026-09-26 p50=31.4 p95=57.9 max=71.2

2026-09-27 p50=22.8 p95=41.0 max=49.6

2026-09-28 p50=32.0 p95=58.6 max=74.0

Repeat the same pattern for the other resources. These are the reports and columns worth summarising:

Resourcesadf reportColumnWhy this one
CPU-u100 - %idle, and %stealBusy time; steal shows a noisy hypervisor
CPU saturation-qrunq-szRunnable tasks; compare against nproc
Memory-rkbavail, %commitMemory the kernel can actually hand out; overcommit pressure
Paging-Wpswpin/s, pswpout/sSustained swap traffic means memory is already short
Disk-daqu-sz, await, %utilQueue depth and latency per device
Network-n DEVrxkB/s, txkB/s, %ifutilThroughput against link speed
sponsored

Fit the Trend Yourself, and Zoom In on Peaks

A least-squares line through the daily p95 values is crude but honest: it is reproducible, and its assumptions are obvious. Compute it with awk rather than asking the model to eyeball a slope:

$ awk '{ split($3, p, "="); x = NR; y = p[2]; n++; sx += x; sy += y; sxy += x*y; sxx += x*x }

END { b = (n*sxy - sx*sy) / (n*sxx - sx*sx); a = (sy - b*sx) / n

printf "slope %.2f pts/day, fitted today %.1f, days to 80%%: %.0f\n", b, a + b*n, (80 - a)/b - n }' cpu_daily.txt

slope 0.21 pts/day, fitted today 58.3, days to 80%: 103

For the busiest hour of the week, sar's ten-minute averages are too coarse. Run vmstat at five-second resolution across the peak and keep the file:

$ vmstat -t -S M 5 720 > vmstat-peak-$(date +%F).log # one hour, MiB units

$ head -3 vmstat-peak-2026-09-30.log

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- -----timestamp-----

r b swpd free buff cache si so bi bo in cs us sy id wa st UTC

11 0 0 812 164 10233 0 0 12 940 8211 15020 71 12 15 1 1 2026-09-30 14:00:05

$ nproc

8

# Share of samples where runnable tasks exceeded the CPU count

# (NR > 3 skips the two header lines and the since-boot line)

$ awk 'NR > 3 && $1 > 8 { hi++ } NR > 3 { n++ } END { printf "%.0f%% of samples r > nproc\n", 100*hi/n }' vmstat-peak-2026-09-30.log

23% of samples r > nproc

The first vmstat line is since boot β€” it reports averages since the system started, not the first interval. Drop it before computing anything.

r is the saturation signal β€” the r column counts runnable and running tasks. Sustained values above nproc mean work is queueing for CPU even if the average busy percentage looks fine.

si and so should be zero β€” any sustained swap-in or swap-out during the peak means the working set no longer fits in RAM, regardless of what free memory says.

Linear is an assumption β€” growth from a product launch is a step, not a slope, and a migration halfway through the window bends the line. Mark known events so you, and the model, can fit before and after separately.

The Prompt: Interpretation, Not Arithmetic

Build one compact input file: the hardware, the daily p95 table for each resource, your computed slopes, and the events you know about. A quarter of daily rows for six metrics fits comfortably in a local model's context. Metrics from production hosts include hostnames and traffic patterns, so a local model is the sensible default.

$ ollama run llama3.1:8b "You are reviewing capacity data for a Linux server.

Do not recompute statistics; the numbers below were computed with awk and are correct.

HOST: web01, 8 vCPU (KVM guest), 16 GiB RAM, 500 GB NVMe, 1 Gbit NIC

WORKLOAD: nginx + PHP-FPM + Redis; traffic grows roughly 5% per month

KNOWN EVENTS: 2026-09-10 PHP 8.3 upgrade; 2026-09-21 marketing campaign start

DAILY P95 (last 28 days):

$(paste -d' ' cpu_daily.txt runq_daily.txt memavail_daily.txt disk_daily.txt)

FITTED TRENDS (least squares on daily p95):

$(cat trends.txt)

THRESHOLDS: CPU busy p95 80%, run queue p95 above 8, memory available below 2 GiB,

disk aqu-sz above 4, NIC utilisation p95 60%.

For each resource answer:

1. Current headroom against its threshold, quoting the numbers given.

2. Whether the trend looks linear, stepped, or seasonal, and why.

3. Which known events probably explain any change, and whether to fit before and after separately.

4. Which resource will cross its threshold first, with the date from FITTED TRENDS.

5. Any metric that could be misleading on this host and what to check instead.

6. What additional data would most change the forecast.

Say UNSURE where the data does not support a conclusion."

Feed it summaries, not sadf dumps β€” a month of ten-minute samples for six reports is tens of thousands of lines. The model will truncate, skim or invent averages. Daily percentiles keep it on interpretation.

Give it the hardware β€” a run queue of 6 means nothing without the CPU count, and %steal means nothing unless the model knows it is a VM.

State thresholds yourself β€” headroom targets are a business decision: how fast you can add capacity, and how much burst you need to absorb. Do not let the model choose them.

Ask for what is missing β€” question 6 is often the most useful answer. Typical replies, such as per-process memory or request rates from the application, point at what to add to collection. The same pattern works for log analysis.

Check the Answer Against the Counters

The model's output is a set of claims about your system. Most of them can be checked against the data in a minute, and the ones that matter most are the places where sysstat counters are easy to misread:

CounterCommon misreadingWhat to check instead
%memused (sar -r)High means memory is shortkbavail and swap traffic; page cache counts as used but is reclaimable
%iowaitHigh means the disk is the bottleneckIt is idle CPU time with I/O pending. Check aqu-sz and await per device
%util (sar -d)100% means the device is saturatedTrue for a single spinning disk. NVMe and RAID serve many requests in parallel; use await and aqu-sz
%stealIgnoredOn a VM, steal above a few percent at peak means the host is oversubscribed; more vCPUs may not help
load averageHigher than nproc means CPU boundLinux load includes tasks blocked in uninterruptible I/O; compare runq-sz and blocked
%commitAbove 100% is an outageOvercommit is normal; it matters when kbavail is also falling

Recompute any number it quotes β€” if the answer says p95 CPU reached 74% on the 28th, grep 2026-09-28 cpu_daily.txt should agree. A misquoted number is a sign the rest of the reasoning was not grounded either.

Refit around events it flagged β€” if the model suggests the trend changed at the PHP upgrade, fit only the days after it and see whether the date to threshold moves. That is a two-line awk change, not a judgement call.

Re-run monthly β€” schedule the export with a systemd timer and keep the prompt in version control. Forecasts that move a lot month to month are themselves a finding.

Cross-check live β€” for current-state questions, the performance analyzer and process explorer show what is consuming a resource right now.

Frequently Asked Questions

Can an LLM forecast server capacity directly from sar files?

Not reliably. Models are poor at computing percentiles and regressions over thousands of rows and will often invent averages. Reduce the data to daily percentiles with sadf and awk, fit the trend yourself, and use the model to interpret the summary, spot misleading metrics and suggest what to measure next.

Where does sar store its historical data?

On Debian and Ubuntu the daily files are in /var/log/sysstat, and on RHEL and Fedora in /var/log/sa, named saDD by day of month. Retention is set by HISTORY in /etc/sysstat/sysstat on Debian, where the default is 7 days, or /etc/sysconfig/sysstat on RHEL, where it is 28.

How do I export sar data to CSV?

Use sadf -d with the data file and the sar report flags after a double dash, for example sadf -d /var/log/sa/sa28 -- -u. The output is semicolon-separated with a header line starting with #. Column positions vary between sysstat versions, so check the header before parsing.

Why use p95 instead of averages for capacity planning?

Averages hide the peaks that users notice. A server averaging 30% CPU can still saturate every afternoon. The daily 95th percentile tracks how busy the busy periods are while ignoring a handful of outliers, and its trend is a better predictor of when you will run out of headroom.

Is %iowait a good measure of disk saturation?

No. %iowait is idle CPU time during which I/O was outstanding, so it falls when the CPU gets busier even if the disk is struggling. For disk capacity, look at per-device aqu-sz and await from sar -d, and treat %util with caution on NVMe and RAID devices that handle requests in parallel.

sponsored

Related Tools

See also