Every few weeks someone asks a version of the same question: is a cheap server enough for my project? The answers are always adjectives. "It depends." "Probably fine for a small site." "You'll want to scale up before launch." Nobody produces a number, so everybody over-provisions, because nobody wants to be the person whose launch fell over.
So I rented the cheapest thing DigitalOcean sells that anyone would actually run a site on — one vCPU, 1GB of memory, $6 a month, which works out at 0.893 cents an hour — put a real application on it, and pushed it until something broke. Then I measured what broke, which turned out to be the interesting part.
The short version: it served 9,737 requests a second, and the first thing to give out was not the hardware.
Making the test honest
Most benchmarks of this shape are useless for two reasons, and both were easy to avoid.
The first is that they serve a hello-world page. I put four workloads on the box instead: a 6.7KB catalogue page served as a static file by nginx, a JSON API endpoint, a SQLite query against a 5,000-row table returning twenty rows, and a server-rendered HTML page built from that same query. Node 22 behind nginx, which is what a small production app actually looks like.
The second is that they run the load generator on a laptop, so what gets measured is the laptop's network. I rented a second, much larger machine — four vCPUs, in the same region — purely to generate load, on the principle that the thing doing the measuring should never be the thing that runs out first. At no point did the generator break a sweat.
Every number below is the mean of three fifteen-second runs, and I kept the raw output rather than a summary, for reasons that become clear later.
The numbers
| workload | 100 connections | 400 connections | p50 at 100 | errors |
|---|---|---|---|---|
| static page (nginx) | 9,737/s ±491 | 9,773/s ±405 | 9.2 ms | 0 |
| JSON endpoint (Node) | 1,291/s ±28 | 672/s ±67 | 67 ms | 0 |
| SQLite query | 1,049/s ±4 | 556/s ±119 | 82 ms | 0 |
| server-rendered HTML | 1,099/s ±39 | 521/s ±109 | 79 ms | 0 |
Across the whole verification run the box served 1,117,976 requests and returned zero non-2xx responses. Not a handful of errors at the top end. None.
Translating that into the units people actually plan with: the static page at 9,737 a second is 35 million requests an hour. The rendered page, the slowest thing I tested, still does 3.9 million an hour. If a visitor views eight pages in a session, the dynamic path supports something like 490,000 visitors an hour at full tilt.
Whatever your side project is doing, it is not doing that.
What actually runs out
I sampled the server itself once a second throughout, which is the part most benchmarks skip, and the answer is clear: it is CPU, and only CPU.
At saturation the single core was 57% in user space and 21% in the kernel, with 2% idle. Memory was never remotely a factor — the lowest free memory I recorded was 596MB of 961MB, with nginx holding 10MB and Node 83MB. The 1GB that sounds so meagre on the pricing page sat two thirds empty while the CPU was pinned.
One number deserves calling out, because shared-CPU instances have a reputation: CPU steal averaged 0.58% across the loaded period. Steal is the time your virtual core is ready to run and the hypervisor gives it to somebody else, and on a noisy host it can be brutal. I saw a single one-second spike of 23% and essentially nothing otherwise. The cheapest shared instance on the platform was not meaningfully robbed of the CPU it was sold.
The nine-fold gap, and the one line that closes it
A static file goes nine times faster than a rendered page. That gap is the whole story of why small servers feel slow: your machine is fine, it is just doing work per request that it could be doing once per ten seconds.
So I added a single directive in front of the rendered route — proxy_cache with a ten-second lifetime — and ran it again.
| rendered HTML | before | after | change |
|---|---|---|---|
| 100 connections | 1,099/s | 18,662/s | 17x |
| 400 connections | 521/s | 18,557/s | 34x |
The cached dynamic page is now faster than the static file, which looks absurd until you notice the rendered response is 566 bytes against the catalogue page's 6.7KB. Less to push down the wire.
The honest caveat: a ten-second cache means somebody can see ten-second-old content. For a catalogue, a marketing page, a leaderboard, a blog, that is free performance. For a dashboard showing someone their own balance it is wrong, and no amount of throughput makes it right.
The only errors I saw all day were from 2004
The cached run at 400 connections was the first time anything failed: 311 non-2xx responses out of 1,651,331 requests. That is 0.019%, and I could have rounded it away. Instead I read the log.
[crit] open() "/var/cache/nginx/app/3/f2/cd07..." failed (24: Too many open files)
[alert] socket() failed (24: Too many open files) while connecting to upstream
[crit] accept4() failed (24: Too many open files)
Not memory. Not CPU. File descriptors. Ubuntu's nginx ships with worker_connections 768, a default that has been carried forward since machines had a fraction of this one's capability, and a per-process file limit to match.
Two lines of configuration later — worker_connections 8192 and a raised LimitNOFILE — I ran the same test again: 840,277 requests, zero errors, and an empty error log, at 18,557 requests a second.
The $6 machine never hit its limit in that test. My configuration did.
The ceiling nobody mentions
Here is the number that should actually shape your planning, and it has nothing to do with requests per second.
While serving the static page, the box pushed 64.6MB a second, which is 517 megabits. The plan includes 1TB of outbound transfer a month. At that rate you would spend the entire monthly allowance in four hours and eighteen minutes.
Put the other way round, which is the useful way: 1TB is about 146 million page views of that size. You will not run out. But if your traffic ever does approach what this hardware can serve, the bill that arrives will be for bandwidth, not for the server. The CPU is not the constraint and neither is the memory — the pipe is, and it is the line item everyone forgets to model.
What I got wrong
Two things, both in my own instruments rather than in the results.
I built a status check that ran pgrep -f verify_run.sh over SSH to see whether the benchmark was still going. It reported RUNNING, confidently, for about fifteen minutes — during which nothing was running at all, because the script had never been copied to the machine. The pattern was matching the SSH command line containing the words verify_run.sh. It was reporting on itself. I replaced it with a marker file that the job touches when it finishes, which cannot lie in that particular way.
Then, converting bandwidth into page views, I divided a terabyte by 10⁹ instead of 10¹² and got 146,391 page views, a number I very nearly wrote down. It is 146 million. The tell was that it felt small, and the honest lesson is that unit errors in your favour are the ones you catch, while unit errors against you slip through because they sound appropriately humble.
So: is a $6 server enough?
For most things people build, comfortably, and it is not close.
A single shared vCPU with a gigabyte of memory served a million requests without a single error, handled a real SQLite-backed page at over a thousand a second, went to nearly nineteen thousand a second once I let nginx cache the output for ten seconds, and spent the whole time with two thirds of its memory free and its CPU steal under one per cent. The parts that did break were a twenty-year-old default in a config file and my own arithmetic.
The useful advice is not "buy the small one". It is that the bottleneck is almost never where the pricing page makes you look. Before you upgrade the machine, check whether you can cache a route for ten seconds, and check whether you are about to pay for a bigger CPU when what you are actually running out of is bandwidth.
Measured on a DigitalOcean s-1vcpu-1gb droplet in Frankfurt at $0.00893/hour, with load generated from a separate s-4vcpu-8gb droplet in the same region. Both destroyed afterwards; the whole experiment cost under twenty cents.
Top comments (0)