Every "how far does a cheap VPS scale" post eventually cites a requests-per-second number, and the number is almost always someone else's, run against an app that isn't yours, on a day nobody documented the conditions for. That number is worthless to you. What's actually useful is understanding the mechanism that sets the ceiling — because once you know what's actually running out, you can reason about your own app instead of importing a stranger's benchmark and hoping it transfers.
So this isn't a load-test writeup. Nobody ran a benchmark for this post, and it says so plainly rather than dressing up a made-up figure as data. It's a mechanism post: what actually caps throughput on a 4 vCPU / 8 GB box, why caching changes the math by orders of magnitude rather than a percentage point, and — because that's the part every benchmark-free post owes the reader — exactly how to go measure your own number. the team at XenGrowth approaches load testing from the operator's side, which complements the engineering view here.
What actually runs out first on a fixed-size box?
Not CPU, usually. That's the counterintuitive part. A Contabo Cloud VPS 4 — 4 vCPU, 8 GB RAM, 100 GB SSD, €5.50/month for the first 24 months, per Contabo's own current pricing — sounds like it should be CPU-bound, and for compute-heavy work it is. But most SaaS request handling isn't compute-heavy. It's I/O-bound: waiting on a database query, waiting on an external API, waiting on disk. During that wait, CPU sits idle while memory stays fully committed to holding the request's state open. On a small box, memory runs out long before CPU does.
Resource | What actually consumes it | What happens when it runs out |
|---|---|---|
CPU (4 vCPU) | Request handling, rendering, compression, encryption | Requests queue and latency climbs; the process itself usually survives |
Memory (8 GB) | Every open connection, every in-flight request's buffers, the runtime itself, and any in-process cache | The kernel's OOM killer terminates a process — often not the one that caused the spike |
Open file descriptors / connections | Concurrent database connections, open sockets, worker pool size | New connections get refused or time out, existing ones keep working |
Disk I/O | Database writes, log writes, local ISR cache reads/writes | Latency spikes across everything sharing that disk, not just the slow query |
That memory-first failure mode is worth taking seriously rather than treating as an edge case, because it doesn't announce itself. There's no warning banner before the kernel kills something — the box looks fine, then a process is dead mid-request, and it's often not the process that actually caused the spike. The full mechanism behind that, and how to read the aftermath, is in what happens when your VPS runs out of RAM.
What determines how many requests you can actually handle at once?
Concurrency, not raw CPU count, is the number that actually matters, and concurrency is set by how many workers you run and how each one handles simultaneous requests. Node.js is single-threaded per process — one process doesn't parallelize CPU-bound work across multiple cores on its own. What it does well is I/O concurrency: while one request waits on a database call, the event loop is free to start handling the next one, which is exactly the shape of most web request handling. The XenGrowth resource library approaches this from the the operations side of this side.
One Node.js process uses the event loop to interleave I/O-bound requests efficiently, but a single process still only uses one CPU core for actual computation
Running multiple worker processes — one per vCPU, roughly — is how you actually use all 4 vCPU on the box, whether through a process manager or multiple containers behind the reverse proxy
Each additional worker also multiplies your memory footprint, because each is a separate runtime with its own heap — this is the direct tradeoff against the memory ceiling above
A database connection pool sized too small serializes requests behind it regardless of how many app workers are running; sized too large, it can itself exhaust the database server's own connection limit
This is why "how many requests per second can a 4 vCPU box handle" doesn't have a universal answer. An app that's mostly waiting on a fast, well-indexed database query can run many concurrent requests per worker, because most of that time isn't consuming CPU. An app doing real computation per request — image processing, complex serialization, cryptographic work — hits the CPU ceiling with far fewer concurrent requests, because there's no idle waiting to interleave around.
Why does caching change the arithmetic instead of just the speed?
Caching doesn't make a request faster in the way people usually picture it — it prevents most requests from reaching the origin server at all. That's a structural difference, not a tuning improvement, and it's why hit ratio moves the ceiling by orders of magnitude instead of by a percentage. A box that can comfortably serve 50 uncached requests per second can serve many times that in effective traffic if 95% of requests are answered at the edge and never touch the VPS.
Next.js's own architecture makes this concrete: ISR and the server-side render cache share one cache, on local disk by default, per the framework's own self-hosting documentation. That's fine for a single instance with persistent disk — cache hits skip rendering entirely and just serve the stored HTML. It stops being fine the moment you run multiple instances of the app behind a load balancer without a shared cache handler, because each instance then holds its own independent, partially-stale copy, and a request that would have been a hit on one instance is a cold miss on another.
Cache layer | What it prevents from happening | What breaks it |
|---|---|---|
Cloudflare edge cache (free tier) | Request from reaching your origin server at all | TTL below the free tier's 2-hour floor, or a response marked uncacheable |
Next.js ISR / server cache | Server-side render work for a request that already has a stored result | Multiple app instances without a shared cache handler, each holding a divergent local copy |
Browser cache | Request from leaving the user's machine in the first place | Missing or overly short cache-control headers on static assets |
One free-tier detail is easy to trip on: Cloudflare's free zones can't set an edge cache TTL below two hours through cache rules, per Cloudflare's own documentation. For content that changes often, that floor is either a feature — it forces you to think about what's actually safe to cache for two hours — or a real constraint that pushes you toward a paid plan's finer TTL control. Either way, it's worth knowing before you design a caching strategy around an assumption the free tier doesn't support. XenGrowth on governed AI marketing workflows approaches this from the AI agents and marketing automation side.
So how do you actually find your own ceiling?
By load-testing your own app against a staging copy of your own infrastructure — not by reading someone else's number and assuming it transfers. This is the part most "scaling" posts skip, because a real number requires running a real test, and a fabricated one is faster to write. Here's what that test actually needs.
Run the test against a staging environment sized identically to production, not against production itself and not against a laptop
Pick a load-testing tool that can ramp concurrency gradually rather than slamming full load instantly — k6, Apache Bench, wrk, or Locust all work; k6 and Locust let you script realistic multi-step user flows instead of hammering one endpoint
Test your actual critical paths, not just the homepage — the endpoint that hits the database and the one that's served entirely from cache will report wildly different ceilings
Watch p95 and p99 latency, not the average — the average hides exactly the tail-latency degradation that means real users are having a bad time while your dashboard still looks fine
Watch error rate and memory usage on the box in parallel with the load test, not just requests-per-second, because the failure mode is often memory exhaustion or connection-pool saturation, not a clean rate limit
Increase load in steps and note where p95 starts climbing sharply rather than gradually — that inflection point, not the eventual failure point, is your real usable ceiling
A benchmark run against someone else's app tells you what their app can do. p95 latency and error rate from your own load test, against your own staging box, are the only numbers that describe your ceiling.
p95 specifically deserves a plain definition, because it gets thrown around without one constantly: it's the latency figure below which 95% of requests complete — meaning 1 in 20 requests is slower than that number, sometimes much slower. That's the number that matters for user experience, because averages get dragged down by the fast, cached majority while the slow tail is exactly what a frustrated user actually experiences.
Does vertical scaling ever actually solve this, or just delay it?
Moving up Contabo's own lineup — from the Cloud VPS 4 at 4 vCPU / 8 GB to the Cloud VPS 6 at 6 vCPU / 12 GB, or further — buys more headroom against every resource in the table above, but it doesn't change which one runs out first. A memory-bound app that's hitting the OOM killer on 8 GB will generally stop hitting it on 12 GB, at least for a while, but if the underlying cause is a memory leak or an unbounded cache rather than genuinely higher legitimate load, a bigger box just delays the same failure to a later, possibly less convenient moment.
That distinction matters because it changes what you should actually do in response to hitting a ceiling. If a load test shows memory climbing linearly with legitimate concurrent users, more RAM is a reasonable, honest fix. If memory climbs over time regardless of load and only a restart brings it back down, that's a leak, and a bigger box just buys a longer runway before the same crash — worth knowing before spending money on vertical scaling that a code fix would have solved for free. There is a longer treatment of AI search, GEO and discovery in XenGrowth on building one SEO and GEO content system.
What does this mean for sizing the box in the first place?
It means the sizing decision isn't "how many vCPU do I need" in isolation — it's a joint decision across vCPU, RAM, worker count, and expected cache hit ratio, and getting one of those wrong doesn't get rescued by the others being generous. A 4 vCPU / 8 GB box running a well-cached, I/O-bound app can comfortably outperform a larger box running an uncached, memory-hungry one. The full framework for making that call — which of vCPU or RAM to prioritize for a given workload shape — is in vCPU or RAM: how to size a VPS, and it's worth reading before you run the load test above, so you know what you're actually looking for in the results.
It's also worth remembering what's actually consuming resources underneath the app itself — the deploy layer, the reverse proxy, and the runtime it's all wrapped in aren't free, and sizing a box for the app alone while forgetting what runs alongside it is a common way to end up short. Docker concepts you need before Coolify covers exactly what's eating memory and CPU besides your application code.
There's no single answer to "how far does this scale," and anyone who gives you one without having run it against your workload is guessing with confidence. Run the test, read p95 instead of the average, and watch memory alongside requests-per-second — that's the whole method, and it works regardless of which box you're on. If you want a second set of eyes on your own load-testing setup or capacity plan, that's the kind of work I take on through services.
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 XenGrowth practice writes for the teams who have to run load testing day to day.
Four questions on where the ceiling actually is on a small box. It's rarely the thing people upgrade for.













