Infrastructure · Security · Engineering Tell us what you need

Home / Guides / Application hosting architecture

Guide · open, not gated

Working out what your
application actually needs.

Almost every hosting decision is made by guessing, and the error tends to fall in the same direction — too much compute, too little database, and no idea what the bandwidth will cost. This guide is how to replace the guess with five measurements.

Version
1.0
Last reviewed
August 2026
For
Anyone sizing infrastructure
Commands
All read-only

Before you run anything

This guide is general information about how we approach sizing. It is not advice for your specific situation, and it is not a substitute for someone who knows your environment.

Every command below is intended to be read-only, and we have written them that way deliberately. You are nonetheless running them on your own systems, at your own discretion. Read each one and understand what it does before you run it — particularly on anything carrying production traffic. If you are not certain, run it somewhere that does not matter first.

Figures, thresholds and worked examples here are illustrative. They come from a constructed example, not from your application, and applying them without measuring your own workload will give you the wrong answer. MDCS accepts no liability for decisions taken on the basis of this guide.

Anything touching regulatory or contractual obligations should be checked with your own advisers.

Why guessing fails in a predictable direction

Infrastructure is usually sized from what is visible. The size of the codebase, the number of registered users, how important the application feels. None of those determine what hardware it needs.

The result is consistent enough to be a pattern: compute is over-bought because it is the number the provider asks for first and capacity headroom feels safer; the database is under-built because it is invisible until it is the bottleneck; and bandwidth is often absent from the estimate until the first bill shows an egress line.

The five numbers that actually decide it

Requests at peak · concurrent users · database working set · monthly egress · storage growth rate. Everything else follows from those. This guide is how to get each one.

Before you start

Everything below is read-only. Nothing here changes a running system, and nothing needs to be installed. If a command asks for a password, it is asking for read access to something you already own.

Two sensible precautions

Run these on a system that is under normal load. Measuring at 6am on a Sunday tells you what your application does when nobody is using it, which is not the thing you are sizing for.

Measure for a week, not an hour. Almost every application has a weekly shape — a Monday morning peak, a month-end batch, a quiet Friday. One hour of data will mislead you in whichever direction that hour happened to fall.

01 · Traffic

What you need: requests per second at peak, and the average, so you know the ratio between them.

If you run nginx

# requests per hour, so you can see the shape of the day awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1,2 | uniq -c # the busiest single minute in the log awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1,2,3 | sort | uniq -c | sort -rn | head -5

If you run Apache

awk '{print $4}' /var/log/apache2/access.log | cut -d: -f1,2 | uniq -c | sort -rn | head -10

Divide the busiest minute by 60. That is your peak requests per second. Divide total requests by the seconds in the period for your average.

Mind the log period. Most systems rotate logs daily, so access.log may hold hours rather than weeks. Check with ls -la /var/log/nginx/ and include the rotated files if you need a longer window. Compressed archives can be read with zcat without unpacking them.

These commands read the whole file. On a large log on a busy machine that is measurable I/O — run it outside peak hours, or copy the log elsewhere and analyse it there.

You also want the p95 response time — the figure 95% of requests come in under. Most application frameworks log it; if yours does not, the browser developer tools will give you a usable approximation for the slowest page.

Why the ratio matters more than either number

An application at 4 requests per second on average and 22 at peak needs to be sized for 22. Sizing for the average under-builds it nearly six times over. Conversely, if peak and average are close, you have a steady workload — which changes the hosting economics entirely, because you are not paying for elasticity you never use.

02 · Utilisation

What you need: actual CPU, memory and disk usage at peak on whatever you run today. This is the measurement that most often changes the specification.

A snapshot, taken during a busy period

# CPU and memory, five samples ten seconds apart vmstat 10 5 # memory in a readable form free -h # disk I/O — install sysstat if iostat is missing iostat -x 10 3 # load average against core count uptime; nproc

Better: a week of history

# sysstat keeps daily history if it is enabled sar -u -f /var/log/sysstat/sa$(date +%d) # CPU sar -r -f /var/log/sysstat/sa$(date +%d) # memory

Compare load average to core count. A load average of 2 on 8 cores is a machine doing very little. The same figure on 2 cores is a machine at its limit.

sar only has history if sysstat was already collecting it. On many systems it is installed but not enabled, in which case those commands return nothing — that is not an error, it means there is no history to read. Enabling collection now gives you data from now on, not retrospectively.

Load average also means different things on different systems. On Linux it counts processes waiting on disk as well as CPU, so a high figure can indicate an I/O problem rather than a CPU one. Read it alongside iostat, not on its own.

The finding that changes most sizing decisions

Application servers commonly sit between 5% and 20% CPU. Meanwhile the database is memory-starved and doing far more disk I/O than it should. Where that is the case, added CPU will not change the response time. The constraint is elsewhere.

03 · Database

What you need: the working set — the portion of your data actually being read regularly — not the total size on disk. This is the single most important number in the whole exercise and the one most often got wrong.

MySQL or MariaDB

-- total size per database SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024/1024, 2) AS gb FROM information_schema.tables GROUP BY table_schema; -- how much of the buffer pool is in use, and whether reads are hitting disk SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; -- connections, against the configured maximum SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';

PostgreSQL

-- size per database SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database; -- cache hit ratio: below about 0.99 means you are reading from disk too often SELECT sum(heap_blks_hit)::float / nullif(sum(heap_blks_hit)+sum(heap_blks_read),0) FROM pg_statio_user_tables; -- connections in use SELECT count(*), (SELECT setting FROM pg_settings WHERE name='max_connections') FROM pg_stat_activity;

Divide buffer pool reads that hit disk by total reads. If the ratio is poor, or the PostgreSQL hit ratio is below about 0.99, your working set does not fit in memory — and that is the problem, not CPU.

Estimating the working set without instrumentation: take the tables your application queries on every request, add their indexes, and add roughly 30% for headroom. For most business applications that lands well under the total database size — often a quarter of it.

These queries read system tables and statistics only. They do not touch your data and do not lock anything. Even so, run them with a read-only account rather than an administrative one — the same principle applies here as anywhere else, and nothing in this guide needs write access.

The cache-hit and buffer-pool figures are cumulative since the database last restarted. A server restarted an hour ago will show poor ratios simply because the cache is cold. Check uptime before drawing conclusions.

Why this saves real money

Sizing database memory against total data rather than the working set is the most expensive difference in this exercise. An 11 GB database with a 4 GB working set needs about 16 GB of RAM, not 32 GB or more. Getting this right roughly halves the cost of the most expensive machine you will buy.

04 · Bandwidth and egress

What you need: monthly outbound traffic in gigabytes. Inbound is almost always free; outbound is what you are billed for and what nobody budgets.

# bytes sent per interface since boot cat /proc/net/dev # monthly totals, if vnstat is installed — worth installing for a week vnstat -m # response sizes from nginx logs, in GB, for the log period awk '{sum+=$10} END {print sum/1024/1024/1024 " GB"}' /var/log/nginx/access.log

Take the log period figure and scale it to a month. If your logs cover a week, multiply by 4.3 — not by 4.

Check your log format first. The $10 above is the bytes-sent field in nginx's default combined log format. If a log_format directive in your nginx.conf has changed the column order, that field is something else entirely and the total will be meaningless. Confirm the format before trusting the number. The same applies to the Apache examples, which assume the default combined format.

Note also what each method includes. /proc/net/dev and vnstat count everything the machine sends — backups, package updates, monitoring, replication. The log figure counts only what your application served. They will not agree, and the difference is often the more interesting number.

Where the cost usually sits

Egress between availability zones, and egress from a database to an application in a different zone, are both charged on most public clouds and neither appears until the bill. So is backup traffic leaving the provider. If you are comparing hosting options, model these explicitly — they are frequently a larger line than the compute.

05 · Storage growth

What you need: gigabytes added per month, not the current total. Sizing on today's figure is what leads to expanding storage under pressure a year later.

# current usage df -h du -sh /var/lib/mysql /var/www/uploads 2>/dev/null # growth, if you have backups from previous months ls -la --block-size=M /backups/ | tail -20

Two backups a month apart give you a growth rate directly. Three months of backups give you a trend, which is what you want, because growth is rarely linear once an application is adopted.

Size for three years at the measured rate. Storage is the cheapest thing to over-buy and the most disruptive to expand under pressure.

Growth is rarely linear. Adoption, a new feature, or a change in what you retain can all step the rate up sharply. Three months of history tells you more than two readings, and any figure you derive is a projection rather than a forecast — revisit it once you have real operating history.

Turning the numbers into a size

These are starting points, not rules. Every one of them should move if your measurements say so.

FromWork outSensible starting point
Peak req/s × p95 responseRequests in flight at once22/s × 0.24s ≈ 5 concurrent. Less compute than the figure suggests
Requests in flightApplication CPUStart at 2–4 vCPU for anything under 20 in flight
Working set + indexes + 30%Database memory4.2 GB working set → 16 GB RAM
Peak concurrent usersConnection pool sizePool size, not one connection per user. Usually 20–50 is ample
Growth rate × 36 monthsDisk3 GB/month from 11 GB → about 120 GB
Monthly egressBandwidth costModel it separately. It does not scale with compute

These figures are from the worked example

The right-hand column comes from the worked example rather than from your application, and is included to show the shape of the calculation. The method transfers; the figures do not.

Comparing cost

We deliberately do not publish provider prices. They change constantly, they are regional, and a stale price table is worse than none. What does not change is the method.

Total what you actually pay today

Then compare over four years, not per month

Owned hardware is capital spent once and used for four to five years. Rented infrastructure is an operating cost forever. Comparing a monthly bill against a purchase price is comparing two different things — amortise the hardware across its life, and add power, space, network and the engineering time to run it.

The conclusion

At low or spiky utilisation, renting wins and it is not close. At steady utilisation above roughly half, owning starts to win — and the crossover point depends entirely on your numbers. There is a calculator that will give you a first answer, and it will tell you to stay where you are if that is what the arithmetic says.

If nothing is running yet

You cannot measure an application that has no users. What you can do is stop pretending otherwise.

On building ahead of demand

An architecture built for a hundred thousand users and run for four hundred costs more, has more failure points, and takes longer to change. Scaling on measurement keeps both the cost and the complexity proportionate.

If you would rather not

Or we can do it with you.

Everything in this guide is what we do at the start of a hosting architecture review. If you have run the measurements and want a second opinion on what they mean, or you would rather hand over the whole exercise, that is the service.

If your numbers say you are fine as you are, that is what we will tell you.