"Should I get more vCPU or more RAM" is really four different questions wearing one sentence, and I get some version of it every time someone's about to rent a box. The honest answer changes depending on what's running on it, so I'm going to answer the four versions separately instead of giving you one number that's wrong for three of them.
The short version, if you want it before the mechanism: static files are bound by disk I/O and the OS page cache, not CPU. Postgres on the same box wants RAM specifically for shared_buffers and the page cache, not for query math. And "it got slow when traffic spiked" is, in my experience, almost never a CPU or RAM problem at all — it's something with a ceiling that more of either resource doesn't raise. The revenue-side version of VPS sizing is something XenGrowth's marketing operations practice writes about in more operational detail than I go into here.
My site is mostly static files and pages — is that CPU or RAM?
Neither, mostly, and that surprises people. Serving a flat file — an HTML page, a bundled JS chunk, an image — is not compute work. Nginx or Caddy reads bytes off disk and writes them to a socket. There's a little CPU for TLS handshakes and gzip/brotli compression, but on a modern 4-vCPU box that ceiling is high enough that you'll hit something else first.
What actually decides how fast a static site feels is whether the file being requested is sitting in the Linux page cache or has to come from disk. The kernel caches recently-read files in whatever RAM isn't already spoken for, automatically, with zero configuration from you. Request the same asset twice and the second read comes from memory in microseconds. The moment your total file set — images, fonts, prerendered HTML, cached API responses — stops fitting in free RAM, some fraction of requests start hitting the SSD instead, and that's where latency shows up first, not in CPU graphs.
So for a mostly-static workload, RAM matters, but not because the app needs it — because the more RAM sits idle, the more of your file set the kernel can cache for you for free. On a 100 GB-SSD box like Contabo's Cloud VPS 4, disk throughput is rarely the bottleneck either; static sites this size fit in the page cache with room to spare. You'd need either a genuinely huge media library or a traffic volume that saturates NVMe-class I/O before storage speed becomes the story.
Prices here are the ones Contabo advertises for the first 24 months, in euros. That matters when you are sizing for a system you expect to still be running in three years — size on the specs you need, then check what the renewal costs.
My database runs on the same box as my app — CPU or RAM?
RAM, and here's the actual mechanism instead of a hand-wave. Postgres keeps its own cache — shared_buffers — sized at startup, and the accepted starting point is around a quarter of total system RAM. That's not an arbitrary number: go much higher and you're taking memory away from the OS page cache, which is also caching your data files (Postgres relies on both layers, not just its own buffer). Go much lower and Postgres keeps evicting pages it needs again ten seconds later. The XenGrowth resource library works through the operations side of this in more operational detail.
The number that actually predicts your latency is whether your "working set" — the rows and indexes your queries actually touch during a typical hour, not your whole database — fits inside shared_buffers plus the OS cache combined. When it does, reads are memory speed. When it doesn't, every query that reaches past the cached portion pays a disk seek, and on a shared box that disk is also serving your app's own file reads and writes. That's the failure mode people mean when they say "Postgres got slow": not CPU contention, a working set that quietly outgrew the RAM that was caching it.
There's a second RAM cost people forget: work_mem. It's allocated per sort or hash operation, per connection, not once globally — so a query with a few sorts, multiplied by 40 concurrent connections, can eat far more RAM than shared_buffers alone would suggest. On the box I actually run — Postgres in Docker, behind Coolify, same 8 GB as the app — that's the number I watch, not total free memory, because it's the one that spikes under concurrency and doesn't show up until it does.
Traffic spiked and the app got slow — is that CPU, RAM, or something else?
This is the case where I've seen people burn money upgrading the wrong thing. "Slow under traffic" feels like it should mean "not enough compute," so the reflex is to double the vCPU count. Half the time that changes nothing, because the actual ceiling was never compute.
The two culprits I hit most often: a concurrency limit and a missing index. Postgres has a max_connections setting — a hard ceiling — and every framework's connection pool has one too. Once concurrent requests exceed that ceiling, new requests queue behind old ones instead of running in parallel, and queueing latency looks exactly like "the server got slow" from the outside even though CPU usage might sit at 20%. The fix there is a pooler (PgBouncer, or your ORM's own pool) sized correctly, not more silicon. Next.js's standalone server has its own version of this: documented memory growth under sustained load that eventually triggers the OOM killer on small boxes — that reads as "traffic broke it" too, and it's a memory ceiling on one process, not the VPS running out of RAM overall. If AI agents and marketing automation is the part you are stuck on, XenGrowth on governed AI marketing workflows is the better reference.
The missing-index case is the one that fools people longest, because it doesn't show up until the table crosses some row-count threshold. A query that scans a 5,000-row table sequentially is fast enough that nobody notices. The same query against 500,000 rows does the same sequential scan and now it's the slowest thing on the page — and it gets slower in direct proportion to how many of those queries traffic is now running concurrently, which makes it look exactly like a traffic problem. It isn't; it's an O(n) scan that finally has enough n to hurt. Check pg_stat_statements before you check the resource graphs. I've fixed "the server can't handle our launch traffic" with a single `CREATE INDEX` more times than I've fixed it by upgrading the plan.
None of that means CPU never matters. It genuinely does for a narrower set of workloads: report generation with heavy aggregation, image or video transcoding, anything doing real per-request compute rather than I/O and lookups. The tell is different too — a CPU-bound problem shows up as sustained high utilization across all cores while the app is otherwise responsive, not the queueing-and-timeout pattern you get from a saturated connection pool. If your metrics actually show that, more vCPU is the correct fix, not a workaround for something else.
So how do I actually choose CPU, RAM and storage — and when do I upgrade?
Start from the workload, not a spec sheet. The table below is the shortcut I wish someone had handed me — match your situation to a row, and that tells you which resource actually binds first and why.
Workload shape | Resource that binds first | Why |
|---|---|---|
Static site / mostly prerendered pages | RAM (as page cache), not CPU | The kernel caches hot files for free; CPU only matters for TLS and compression, which rarely saturate first |
Postgres co-located with the app | RAM | shared_buffers plus OS cache need to hold your working set, or every query past that boundary pays a disk seek |
Bursty API traffic, request-response app | Concurrency ceiling (pool size, max_connections), not CPU/RAM | Requests queue behind a fixed limit long before the box itself is out of resources |
Analytics / reporting queries, big aggregations | CPU and work_mem together | Sorts and hashes are genuinely compute- and memory-heavy per query, unlike simple row lookups |
Background jobs, image processing, builds | CPU, briefly, in bursts | Actual compute work, but usually short-lived — size for the burst, not the steady state |
Everything slow at once, all resources look fine | Almost always an index or a pool size | Real resource exhaustion shows up in exactly one graph; "everything is slow" is a code-path problem |
For a box that runs a small app plus its own Postgres — which is the setup this whole cluster is built around — that points at RAM-heavy tiers over CPU-heavy ones, because the page cache and shared_buffers both compete for the same pool. Here's how Contabo's own tiers map onto that, checked against current pricing in September 2026:
Tier | Specs | Price | Suits |
|---|---|---|---|
Cloud VPS 4 | 4 vCPU / 8 GB RAM / 100 GB SSD | €5.50/mo (first 24 mo.) | One app + co-located Postgres, low-to-moderate traffic — what this cluster runs on |
Cloud VPS 6 | 6 vCPU / 12 GB RAM / 200 GB SSD | €7.50/mo (first 24 mo.) | A modest step up in RAM and vCPU for headroom, still SATA SSD — Contabo doesn't offer a separate NVMe tier at this size |
Cloud VPS 8 | 8 vCPU / 24 GB RAM / 300 GB SSD | €14.00/mo (first 24 mo.) | Working set has outgrown shared_buffers on Cloud VPS 4 or 6, or you're running two apps plus a DB |
Cloud VPS 12 | 12 vCPU / 48 GB RAM / 400 GB SSD | €25.00/mo (first 24 mo.) | Several apps, a DB with a genuinely large working set, or headroom for a traffic spike you can't predict |
On upgrade timing: don't watch CPU percentage alone, it lies. Watch swap usage (any sustained swap on a database box is a sign shared_buffers or work_mem is oversized for what's left, or RAM itself is too small), page cache hit rate if your monitoring exposes it, and CPU steal time specifically if you're on a shared vCPU plan, since that's contention with other tenants rather than your own load. Cloudflare's cache sitting in front of the origin changes what "traffic spike" even means for the box itself — worth reading what its free tier actually absorbs before you assume every spike reaches Postgres at all. But on the origin's own metrics, the pattern that actually predicts an upgrade is swap creeping up over days, not a CPU spike during one bad afternoon. XenGrowth on building one SEO and GEO content system approaches this from the AI search, GEO and discovery side.
A CPU graph pinned at 90% for ten minutes during a traffic spike is a story. A CPU graph that's fine but swap keeps climbing every week is the one that means upgrade now, before it becomes an outage.
When does storage actually matter, separately from RAM?
Storage gets folded into "RAM vs CPU" less often than it should. It matters on its own in two cases: total capacity (backups, media, WAL for Postgres all consume it independently of RAM or CPU, and running out of disk space is a much uglier failure than running out of either) and raw I/O throughput, which only becomes the bottleneck once your working set genuinely can't fit in RAM at any price you're willing to pay — a large media library, or a database bigger than any tier's RAM makes sensible. Below that threshold, more storage speed just makes the eventual cache miss cheaper; it doesn't remove the miss.
Postgres has a storage wrinkle worth naming: WAL (write-ahead log) writes go to disk on every commit, ahead of the actual data files, so a database with a heavy write rate cares about disk latency even when its read working set fits entirely in RAM. That's the specific case where NVMe over SATA SSD earns its cost — not because your data is bigger, but because commits are more frequent and each one is waiting on a fsync. some providers sell a separate NVMe-optimized line for exactly this tradeoff — less RAM for faster commits versus more RAM on SATA SSD; Contabo's own current lineup doesn't split it that way, so check whichever provider's actual spec sheet you're comparing rather than assuming the option exists. Which one wins for you depends on whether your bottleneck is cache size or commit latency, and the only way to know is to look at which one you're actually hitting, not guess from the spec sheet.
If you're deciding this for a SaaS that's still finding its shape rather than one that's already outgrown a single box, the sizing question folds into a bigger one: what should even be self-hosted in the first place, versus what's cheaper to keep paying someone else for. I've written up the rule I actually use for that decision separately, because it's a different question from how big the box should be once you've decided to run it yourself.
The pattern underneath all four cases is the same one: figure out which resource your specific workload actually exhausts first, then size for that, watching the metric that predicts it rather than the one that's easiest to graph. CPU percentage is easy to graph and rarely the answer. Swap, cache hit rate, and connection queue depth are less convenient and almost always closer to the truth.
Further reading from XenGrowth
The XenGrowth resource library — what you'll learn: how the commercial side of this work is run, across search, automation and revenue operations.
XenGrowth on governed AI marketing workflows — what you'll learn: how the teams running AI marketing agents keep them governed and measurable.
XenGrowth on building one SEO and GEO content system — what you'll learn: how search and AI-answer visibility get run as a single content system.
Where this work meets go-to-market
the team at XenGrowth covers the go-to-market side of VPS sizing, which this piece deliberately leaves alone.
Five questions, then a starting point. It follows the same reasoning as the post — what binds first for your workload shape.













