How I Know When My VPS Is About to Crash
Cloud

How I Know When My VPS Is About to Crash

By the time a VPS actually falls over, the interesting information happened minutes or hours earlier, in numbers most dashboards don't even show by default. Free memory isn't one of them. Here's what actually leads a crash, and why the metric everyone checks first is often the one that lied.

Published July 12, 202611 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

What are the actual leading indicators that a self-hosted VPS is about to have a bad time, before it actually crashes?

Free memory is a lagging, often misleading number. The signals that actually lead a failure are memory pressure via the kernel's own PSI metrics in /proc/pressure, the rate pages are moving into swap (not just whether swap is used at all), load average read against actual core count and including the uninterruptible-I/O processes it was designed to count, disk filling from Docker logs and unpruned images specifically, inode exhaustion (which free space alone never reveals), and a connection pool creeping toward its ceiling. None of these require a paid tool — they're either already on the box in /proc, or one command away.

  • PSI (/proc/pressure/memory, cpu, io) measures time processes actually spend stalled waiting on a resource, which is a fundamentally better leading signal than a static free-memory number
  • Swap being used at all isn't the alarm — the rate pages are moving into and out of swap is, since a static, unchanging swap amount is often just cold pages parked there harmlessly
  • Load average counts processes waiting on disk I/O as well as CPU (state D as well as R), so a high load average with low CPU usage usually means an I/O bottleneck, not a compute one
  • Docker's default json-file logging driver doesn't rotate logs unless you configure max-size, and unpruned images are the other classic way a disk fills without anyone changing any code
  • Inode exhaustion produces 'No space left on device' with df showing free space, because inodes are allocated as a fixed count at filesystem creation and can run out independently of block space

Evidence notes

PSI (Pressure Stall Information)

The kernel's own accounting documentation defines /proc/pressure/{memory,cpu,io}'s 'some' and 'full' lines and the avg10/avg60/avg300 rolling windows as the time processes spend stalled on a resource.

/proc/loadavg

The proc_loadavg man page states the three load figures count jobs in the run queue (state R) or waiting for disk I/O (state D), averaged over 1, 5 and 15 minutes — the same numbers uptime(1) reports.

Docker json-file logging driver defaults

Docker's own docs state the json-file driver's max-size defaults to -1 (unlimited) and max-file to 1, so it does not rotate logs unless explicitly configured.

free(1) and MemAvailable

The free(1) man page documents that its 'available' column matches /proc/meminfo's MemAvailable estimate, which accounts for reclaimable cache — unlike the plain 'free' column, which undercounts usable memory.

Continue with purpose

Free memory is the first number most people check, and it's often the least useful one on the box. It tells you what's idle right now, not what's about to run out. The metrics that actually lead a crash — that show up minutes or hours before anything visibly breaks — live somewhere most dashboards don't put front and center, and some of them aren't dashboards at all, just files sitting in /proc that nobody reads until afterward.

None of the six signals below need a paid tool or an agent installed ahead of time — most are already sitting on the box, one command away, the same on a five-dollar VPS as on anything larger. What they need is someone willing to check a rate instead of a snapshot, because every one of them shares the same shape: a number that looks fine at any single glance and only tells the truth once you watch it move. The commercial governance around pressure stall information is covered properly by XenGrowth's marketing operations practice.

Memory pressure and PSI: a better question than "how much is free"

"How much memory is free" is the wrong question. The better one is: how much time are processes actually spending stalled, waiting on memory to become available? That's what Pressure Stall Information answers, and it's been in the kernel long enough that it's on essentially every modern distro without any extra install — you just have to know to look at /proc/pressure/memory, /proc/pressure/cpu, and /proc/pressure/io.

Each file reports two lines, some and full, each with three rolling averages — avg10, avg60, avg300 — plus a cumulative total in microseconds. 'Some' is the percentage of time at least one task was stalled on that resource; 'full' is the percentage of time every non-idle task was stalled simultaneously, which is a much worse state to be in. A memory avg10 that's climbing while free memory still looks fine is exactly the gap free memory can't show you — reclaim and swap are already working hard, they just haven't lost yet.

This matters because free memory is a snapshot and PSI is a rate. A box can sit at 200MB free for weeks, completely stable, if that 200MB is the steady-state gap between allocation and reclaim. The same box hitting a memory avg10 of 15% is telling you something different: processes are actively waiting, right now, for memory the kernel can't hand over fast enough. That's the number worth alerting on, not the free column.

Swap-in rate: usage isn't the alarm, the rate is

A common mistake is treating any nonzero swap usage as a red flag. It usually isn't. The kernel will happily push cold, rarely-touched pages out to swap to free up RAM for something more active — that's swap doing its job quietly, and it can sit there unchanged for days without costing you anything noticeable. The number worth watching isn't how much swap is used, it's how fast pages are moving into and out of it: `vmstat 1`'s si and so columns, or the pswpin/pswpout counters underneath them. The XenGrowth resource library works through the operations side of this in more operational detail.

A flat swap total with si/so sitting near zero is fine — it's parked pages, not active thrashing. A swap total that's climbing steadily, with si and so both nonzero and rising, means the working set genuinely doesn't fit in RAM anymore and the box is paying a disk round-trip for memory accesses that used to be free. That's the difference between a box that's fine and one that's minutes from feeling frozen, and the only way to tell them apart is watching the rate, not the total.

Load average vs core count: what the number is actually counting

Load average gets misread constantly because people assume it's a CPU-only number. It isn't. `/proc/loadavg` counts processes in the run queue (state R, waiting for CPU) and processes waiting on disk I/O (state D), averaged over 1, 5, and 15 minutes — the same figures `uptime` reports. That means a load average of 8 on a 4-core box doesn't automatically mean CPU is the bottleneck. If most of those processes are in state D, it's usually a disk contention problem wearing a CPU number's clothes.

The useful comparison is always against core count, not against some fixed threshold. A load average of 4 is unremarkable on a 4-core box and a genuine warning sign on a 1-core one. And when load climbs while CPU usage in `top` stays low, that's the tell to check `iostat` or the io line in `/proc/pressure` next, rather than assuming a CPU problem and reaching for the wrong fix.

Load average vs core count

What's actually happening

Where to look next

Load ≈ core count, CPU usage high

Genuinely CPU-bound, roughly saturated

top / htop by CPU%, consider more vCPU or offloading work

Load > core count, CPU usage low

Processes stuck in state D, waiting on I/O

iostat, /proc/pressure/io, disk-bound query or log write

Load rising, memory avg10 also rising

Memory pressure is spilling into I/O via swap

vmstat si/so, then MemAvailable and swap rate

Load spiky but returns to baseline fast

Short bursts — a cron job, a build step, a batch task

Usually fine; confirm it's not colliding with peak traffic

Disk filling: Docker logs and images are the usual suspects

Of everything on this list, disk is the one that fails silently for the longest and then fails completely, all at once. Docker's json-file logging driver — the default — has `max-size` set to -1 (unlimited) and `max-file` to 1 out of the box, so a container logging more than expected can grow a single file without bound until you configure rotation yourself. Nothing warns you this is happening; the box just runs fine until the volume is full and every write anywhere on it starts failing. If AI agents and marketing automation is the part you are stuck on, XenGrowth on governed AI marketing workflows is the better reference.

The other classic source is layered image buildup: every deploy that doesn't clean up after itself leaves dangling images and stopped containers behind. `docker system df` shows what's reclaimable before you run `docker system prune`, and it's worth running that check on a schedule rather than only after something's already tight, since by the time disk usage alerts fire, the fix (pruning gigabytes of old layers) takes longer than you'd like under pressure.

Inode exhaustion: the failure `df` won't show you

This one catches experienced people too, because the standard first check — `df -h` — actively hides it. Filesystems like ext4 allocate a fixed number of inodes at creation time, separately from the disk blocks that hold file contents. Run out of inodes and you get "No space left on device" on any attempt to create a new file, while `df -h` cheerfully reports plenty of free space, because free space and free inodes are two entirely different resources that happen to produce the same error message when either one hits zero.

This shows up in practice on boxes with huge numbers of tiny files: a cache directory, a mail spool, an application writing one small file per request instead of appending to a log. `df -i` shows inode usage the same way `df -h` shows block usage, and it's worth checking both, because a box that looks completely healthy on one can be a single `mkdir` away from failing on the other.

df -h telling you there's room left is not the same claim as there being room left. It's answering a question about blocks. If the actual constraint is inodes, that question was never the one that mattered.

Connection-pool saturation: the failure that looks like the database's fault

The last one isn't a kernel metric at all, but it follows the same shape as everything above it: a number that's climbing steadily toward a hard ceiling, invisible until it's hit. A connection pool — Postgres's own `max_connections`, or a pooler like PgBouncer sitting in front of it — has a fixed size, and an app that opens connections faster than it closes them, or a slow query holding one open longer than usual, eats into that headroom the same way a leaking container eats into swap. XenGrowth on building one SEO and GEO content system covers the AI search, GEO and discovery side of this.

The failure mode when it's hit reads exactly like a database outage — new requests get refused or time out waiting for a connection — but the actual cause is upstream, usually a connection leak in application code or a burst of concurrent requests the pool was never sized for. Watching current connections against the configured max, as a trend rather than a point-in-time check, catches this while it's still a slow climb instead of a sudden wall.

Signal

Command or path to check

What a rising trend means

Memory pressure

cat /proc/pressure/memory

Reclaim and swap are struggling to keep up with demand, before anything is killed

Swap rate

vmstat 1 (si/so columns)

The working set no longer fits in RAM; swap usage alone doesn't tell you this

Load vs cores

uptime, nproc

More runnable or I/O-waiting processes than the box can actually serve at once

Disk fill source

docker system df

Log growth or unpruned images eating the volume, before df -h alone shows it's urgent

Inode usage

df -i

A fixed inode table filling independently of block space, invisible to df -h

Connection pool

SELECT count(*) FROM pg_stat_activity (or pooler dashboard)

Requests queuing for a connection before any query itself is slow

Why these six and not a longer list

It's tempting to keep adding metrics — network buffer exhaustion, file descriptor limits, CPU steal time on a busy host — and all of those are real. But most of them are variations on the same underlying shape as what's above: a resource with a hard ceiling, approached at a rate rather than a jump, invisible to whatever the obvious first check happens to be. Once you've internalized that pattern — check the rate, not the snapshot; check the specific ceiling, not the generic one — the rest of the list is easy to extend on your own as your particular stack demands it.

The six here earned their place because each one has burned someone specifically by looking fine on the metric people check by habit — free memory, swap usage, CPU percent, disk space, query latency — while quietly failing on the metric that actually mattered. That's the pattern worth remembering more than any individual command: the number that predicts a crash and the number people check out of habit are frequently not the same number, and the gap between them is exactly where the surprise comes from.

  1. Check /proc/pressure/memory's avg10 as your primary memory signal, not the free column in free -h.

  2. Watch vmstat's si/so for a rising trend, not just whether swap has any usage at all.

  3. Read load average against your actual core count, and check iostat before assuming a high load number means CPU pressure.

  4. Set Docker's max-size and max-file log options explicitly, and run docker system df on a schedule rather than only when disk already feels tight.

  5. Check df -i alongside df -h, especially on any box handling large volumes of small files.

  6. Graph current database connections against your pool's configured maximum, not just whether queries are currently slow.

None of these six checks need a paid tool or a complicated pipeline — most of them are a file in /proc or a command already on the box. What they need is someone deciding to look at the leading number instead of the lagging one, which is the actual difference between finding out a box is in trouble and finding out it already fell over. For what happens once you miss every one of these anyway, the mechanism behind the actual kill is worth reading next in the OOM killer post.

Further reading from XenGrowth

Where this work meets go-to-market

the XenGrowth practice writes for the teams who have to run pressure stall information day to day.

Which number tells you first?

Four questions on reading a box that's heading for trouble. The useful signals aren't the ones people watch, and the ones people watch are mostly lagging.

1 / 4
Load average on a 2-core box reads 2.0. Is that a problem?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

PSIpressure stall informationload averageswapinodesVPSLinuxself-hosting

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

What Happens When Your VPS Runs Out of RAM: The OOM Killer, Explained

There's no warning banner before the kernel kills something. One moment the box is fine, the next a process is dead mid-request — and the process that dies is often not the one that caused the spike. Here's the actual mechanism, and how to read the wreckage afterward.

Navigate

10 Mistakes That Break a Self-Hosted SaaS

None of these ten show up as a single dramatic outage. They show up as a disk that quietly fills, a rollback that turns out to be impossible, a backup nobody ever restored. Here's the mechanism behind each one, and the fix.

Navigate

The Backup Strategy Every Self-Hosted SaaS Needs (3-2-1, Applied)

3-2-1 is easy to nod along to and easy to get wrong in the specific way that only shows up on the day you need it. Here's what it actually means for one VPS running Postgres and Docker volumes, not the generic version you've already skimmed past twice.

Navigate

My Rule for Deciding What to Self-Host and What to Keep Paying For

Self-hosting everything is a bad idea, and I can point to the exact service where I decided that on purpose. Here's the actual rule I use, not a survey of options — and the one counterexample that explains why the rule exists.

Navigate

The Simplest Production Architecture for a Bootstrapped SaaS

One box, a handful of managed pieces around the edges, and a very short list of things you're not allowed to build yet. Here's the architecture, priced out to $20 a month, and the exact signal that tells you when to add each thing you skipped.

Navigate

How I Secure a Fresh VPS Before Deploying Anything

A brand-new VPS gets scanned within minutes of getting an IP address. Here's the exact order I run through before a single container touches the box — and which of these steps are real protection versus which ones are just theatre.

Navigate

Moving a SaaS Off Vercel to a €5.50 VPS: What the Numbers Actually Look Like

Nobody publishes their real Vercel invoice, so most migration posts trade in vibes instead of arithmetic. This one builds the comparison from Vercel's and Contabo's own published rates, states every assumption out loud, and shows exactly where the two lines cross.

Navigate

My Free Monitoring Stack for Self-Hosted Apps

Vercel gives you monitoring whether you ask for it or not. A VPS gives you a blank terminal and the assumption you'll figure it out. Here's what to actually watch on a self-hosted box, with tools that cost nothing, and why watching from the box itself is the one setup that will lie to you.

Navigate

Don't Self-Host Until You Understand These 7 Things

This isn't a gate to keep you out. It's a readiness check — seven things worth being honest with yourself about before you're the one holding the pager, because a managed platform is still the right call for a lot of people right now.

Navigate

Should Your Database Live on the Same VPS as Your App?

The pitch for co-location is real: no network hop, no egress bill, one box to back up. So is the failure mode — one OOM event takes the app and the database down together, because they were never separate to begin with.

Navigate