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 / Ubuntu | RHEL / Fedora | |
|---|---|---|
| Data files | /var/log/sysstat/saDD | /var/log/sa/saDD |
| Config | /etc/sysstat/sysstat | /etc/sysconfig/sysstat |
| Default HISTORY | 7 days | 28 days |
| Collection | sysstat-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:
| Resource | sadf report | Column | Why this one |
|---|---|---|---|
| CPU | -u | 100 - %idle, and %steal | Busy time; steal shows a noisy hypervisor |
| CPU saturation | -q | runq-sz | Runnable tasks; compare against nproc |
| Memory | -r | kbavail, %commit | Memory the kernel can actually hand out; overcommit pressure |
| Paging | -W | pswpin/s, pswpout/s | Sustained swap traffic means memory is already short |
| Disk | -d | aqu-sz, await, %util | Queue depth and latency per device |
| Network | -n DEV | rxkB/s, txkB/s, %ifutil | Throughput against link speed |
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:
| Counter | Common misreading | What to check instead |
|---|---|---|
| %memused (sar -r) | High means memory is short | kbavail and swap traffic; page cache counts as used but is reclaimable |
| %iowait | High means the disk is the bottleneck | It is idle CPU time with I/O pending. Check aqu-sz and await per device |
| %util (sar -d) | 100% means the device is saturated | True for a single spinning disk. NVMe and RAID serve many requests in parallel; use await and aqu-sz |
| %steal | Ignored | On a VM, steal above a few percent at peak means the host is oversubscribed; more vCPUs may not help |
| load average | Higher than nproc means CPU bound | Linux load includes tasks blocked in uninterruptible I/O; compare runq-sz and blocked |
| %commit | Above 100% is an outage | Overcommit 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.