TudCloud Inc.
All articles

Diagnose Linux VPS Performance with PSI and cgroup v2

TudCloud Editorial6 min read
A server tower linked to three modular application containers on an illuminated platform.

A slow application on a VPS does not automatically need a larger plan. It might be waiting for CPU time, reclaiming memory, blocked on storage, or constrained by a service limit while other resources remain idle. This guide builds an evidence trail from a latency symptom to a specific bottleneck.

Scope: Linux with systemd and cgroup v2. Run the inspection commands on the VPS host as an administrator. Container views may hide ancestor limits. These commands inspect state; they do not change resource limits.

Start with a time window, not a screenshot of top

Record the affected endpoint, request rate, error rate and p95 latency during a slow interval. Use the same interval for OS measurements. A daily CPU average can conceal a ten-second burst that exceeds every request timeout.

date -Is
uname -r
stat -fc %T /sys/fs/cgroup
uptime
vmstat 1 11
cat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io

The filesystem check should report cgroup2fs. If pressure files are absent, the kernel or environment may not expose PSI; absence is not evidence of zero pressure. With vmstat, treat the first report separately because it summarizes activity since boot. Keep the following interval reports.

Collect a quiet baseline as well. A number becomes useful when it changes with the symptom and returns toward baseline after a targeted correction. Avoid running a synthetic stress test on a production VPS just to produce interesting graphs.

Understand what PSI measures

Pressure Stall Information measures time lost waiting for CPU, memory or I/O. The some line describes periods when at least one task is stalled; full describes simultaneous stalls of all non-idle tasks in the measured scope. System-level CPU full is not a useful saturation signal. The averages are percentages over recent windows; total is cumulative stall time in microseconds. See the kernel PSI documentation.

For an illustrative 10-second sample, if memory some total increases by 700,000 microseconds, the interval experienced 7% memory-pressure time: 700,000 ÷ 10,000,000 × 100. That is not “7% RAM used,” and it does not identify which allocation caused the stall. CPU, memory and I/O pressure can overlap; adding their percentages creates a misleading total.

Check whether one service is limited

A multi-vCPU machine can look lightly loaded while a single application is constrained. First identify its real service name with systemctl list-units --type=service. Replace nginx.service below with the affected application service, not automatically the front-end proxy.

unit=nginx.service
cg=$(systemctl show "$unit" -p ControlGroup --value)
if [ -n "$cg" ] && [ "$cg" != / ] && [ -d "/sys/fs/cgroup$cg" ]; then
  base="/sys/fs/cgroup$cg"
  for name in cpu.max cpu.stat cpu.pressure memory.current memory.high memory.max memory.events memory.pressure io.pressure; do
    if [ -r "$base/$name" ]; then
      printf 'n%sn' "$name"
      cat "$base/$name"
    fi
  done
else
  printf 'No active service cgroup found; check the unit name.n'
fi

In cgroup v2, cpu.max is quota and period in microseconds. An illustrative 50000 100000 permits half a CPU worth of time per period across the group; max means no quota at that level. Compare deltas in cpu.stat, especially nr_periods, nr_throttled and throttled_usec. Limits also apply through ancestor groups. A child showing max does not override its parent’s restriction. Memory high events indicate throttling/reclaim pressure; oom_kill records kills. Consult the cgroup v2 interface documentation.

Repeat the capture after a known interval. An old nonzero OOM counter is historical evidence, not proof of today’s incident. Similarly, the fraction of periods that were throttled is not the fraction of user requests delayed. Correlate counter changes with application latency.

Separate four commonly confused causes

Evidence during the incident Working hypothesis Next check
Service throttling counters rise; host has spare capacity A quota or ancestor limit is binding Inspect service/container configuration and parent cgroups
CPU pressure rises; runnable work stays high Too much runnable work for available CPU time Find expensive requests, worker concurrency and scheduled jobs
Memory pressure and reclaim-related events rise Working set or service limit is too small for concurrency Compare per-worker memory, cache growth and request volume
I/O pressure rises with storage-heavy work Tasks are waiting on the storage path Check database activity, backups and device latency together

CPU steal is a separate guest scheduling observation: when exposed, it reports time the virtual CPU was not available to the guest. It does not by itself identify another tenant or prove host overselling. Likewise, low steal does not exclude a guest cgroup quota. Send a provider timestamps and repeated samples rather than a single alarming number.

Make one reversible change

Suppose a queue worker starts a large import every hour and web latency increases at the same time. Pause or reschedule that noncritical import for one comparable interval. If pressure and latency fall together while incoming traffic stays similar, you have stronger evidence than an arbitrary global sysctl change.

If the application is quota-bound, inspect how the limit was set before changing it: systemd drop-ins, a container manifest or an orchestrator may reapply it. Record the existing setting and change it through that management layer. More quota can increase competition with the database; more workers can make memory pressure worse. A useful experiment defines the rollback condition in advance, such as higher error rate or new OOM events.

Verify the outcome and decide whether to resize

Compare equal-duration intervals with similar request mixes. Record request throughput, p95 latency, errors, PSI deltas and service counters. Keep cold-start effects and background jobs visible in the notes. Improved average latency with worse tail latency is not an unqualified success.

Resize when sustained useful work exceeds the available resource after avoidable waste and accidental limits are addressed. Choose the resource the evidence identifies: CPU for runnable compute, RAM for a legitimate working set, or a suitable storage tier for measured I/O demand. A larger VPS cannot repair an unbounded query or a forgotten half-CPU quota.

Continue with PostgreSQL query-plan diagnosis when the database drives the load, or TCP connection diagnosis when failures begin before a request reaches the application.