Numbers to Know
Use dated hardware and AWS specifications to check CPU, memory, network, storage, and availability budgets.
Capacity numbers help reject impossible designs and choose something worth testing. A disk's provisioned IOPS, a server's network baseline and an application's measured requests/s answer different questions. Treating them as interchangeable produces confident estimates with no connection to the bottleneck.
Specifications checked: 2026-11-08. Every workload below is a planning assumption; none is a benchmark result. The local examples need Python 3.11+ with no additional packages. Download the complete capacity lab, extract it, and run python3 capacity.py and python3 -m unittest -v test_capacity.py from its directory. AWS commands are optional inspection templates requiring AWS CLI v2, appropriate read permissions and explicit resource values. No cloud account is needed for the lab.
Read the unit before the headline
Label a number before using it: a named model's capability, a published service limit, a sustained baseline or temporary burst, a measurement under stated conditions, or an assumed design budget. A vCPU allocation does not predict requests/s, and a quota increase does not remove a hot key.
Use decimal units for network rates and binary units when specified for memory or storage: 1 Gbit/s is 125 MB/s before protocol overhead; 125 MiB/s is 131.072 MB/s. A vCPU is not universally a physical core. Inspect architecture and threads/core; CPU seconds/request means processor time consumed, including parallel CPU work, rather than elapsed request time.
For storage, operations and bytes impose separate bounds. Assuming uniform I/O sizes, this calculation takes the tightest of the volume's operation limit, volume bandwidth and an explicitly supplied instance bandwidth budget:
def io_ceiling(iops, block_kib, volume_mib_s, instance_mib_s):
return min(iops,
volume_mib_s * 1024 / block_kib,
instance_mib_s * 1024 / block_kib)
assert io_ceiling(3000, 16, 125, 1000) == 3000
assert io_ceiling(3000, 256, 125, 1000) == 500
assert io_ceiling(3000, 256, 125, 100) == 400
# 16-KiB I/O: 46.875 MiB/s. 256-KiB I/O: bandwidth binds first.The 1,000 and 100 MiB/s instance budgets here are assumed inputs. This is an upper bound: queue depth, synchronization, filesystem overhead and actual device latency may prevent reaching it. Database operations also cause varying numbers of physical I/Os; 500 storage IOPS does not mean 500 SQL queries/s. Storage explains the interfaces and database models; here the useful skill is matching quantities with their units.
A dated reference card
These selected capabilities give estimates scale without inventing universal RAM, SSD or network latencies. The models are examples, not claims about the newest or largest available hardware.
| Published specification | What you can infer |
|---|---|
EC2 m7g.large: 2 vCPU, 8 GiB; network baseline/maximum 0.937/12.5 Gbit/s. m7g.xlarge: 4 vCPU, 16 GiB. | These Graviton3 models have one thread/core. Small-instance network bursts consume credits; flow and destination limits also matter. |
EC2 m8g.48xlarge: 192 vCPU, 768 GiB, 50 Gbit/s network. | A named Graviton4 capability, rather than a global EC2 ceiling or application benchmark. |
| Fargate: 1 vCPU with 2–8 GB; 4 vCPU with 8–30 GB. The current table includes 32 vCPU with 60, 120 or 244 GB on Linux platform ≥1.4.0. | Valid task combinations differ from account quotas and regional availability. AWS labels these values “GB”; inspect task-definition memory in MiB rather than assuming decimal bytes. |
| EBS gp3: included 3,000 IOPS and 125 MiB/s; provisionable to 80,000 IOPS and 2,000 MiB/s; size 1 GiB–64 TiB. | gp3 does not use burst credits. Maximum IOPS requires at least 160 GiB; maximum throughput requires at least 8,000 provisioned IOPS and 16 GiB. Instance limits still apply. Outposts maxima are lower: 16 TiB, 16,000 IOPS, 1,000 MiB/s. |
| RDS PostgreSQL gp3: 20–399 GiB includes 3,000 IOPS/125 MiB/s; 400–65,536 GiB includes 12,000 IOPS/500 MiB/s. | Engine-specific provisioning rules differ from standalone EBS. More provisioned capacity does not remove instance or query limits. |
| S3 general-purpose performance guidance: at least 3,500 write-family or 5,500 GET/HEAD requests/s per partitioned prefix. | Gradual scaling can produce 503 responses; this is guidance, not a fixed quota or immediate guarantee. KMS request limits can bind separately. |
| DynamoDB on-demand default per-table quota: 40,000 read request units/s and 40,000 write request units/s, adjustable. | Consumption depends on item size, consistency and indexes. Hot partitions and configured throughput caps can throttle below that total. |
Fargate Linux supports ARM64 and X86_64, while Windows requires X86_64. Availability still needs checking for the chosen Region, AZ and platform. In particular, the ARM64 guide excludes the stable AZ ID use1-az3; account-specific AZ names are not portable identifiers. Validate container images, native dependencies and database extensions for the selected architecture.
The lab checks gp3's size and performance ratios, including accepting 160 GiB/80,000 IOPS/2,000 MiB/s and rejecting the same IOPS at 159 GiB. A valid volume combination still needs a suitable attachment and location. Do not transfer EC2 gp3 limits directly to RDS.
Three workloads worth testing
Derive demand before naming a configuration. All three candidates assume us-east-1, one backing operation for each cache miss or write, and a CPU target of 50%. Requests/user/day count requests reaching the application after any CDN. Read hits below describe an application cache. Response bytes exclude headers, retries and replication. The next lesson, Back-of-the-Envelope Calculations, develops demand estimation in more depth.
| Assumed inputs | A: small txn API | B: read-heavy API | C: keyed content API |
|---|---|---|---|
| DAU range; evaluated DAU | 1–10k; 10k | 10–100k; 100k | 100k–1M; 1M |
| Requests/user/day; peak factor | 20; ×10 | 50; ×8 | 100; ×6 |
| Reads/writes; read-cache hits | 80/20; 75% | 90/10; 90% | 90/10; 95% |
| Response; CPU/request | 2 KiB; 10 ms | 4 KiB; 8 ms | 8 KiB; 5 ms |
| Working set, excluding overhead | 2 GiB DB; 0.5 GiB cache | 12 GiB DB; 4 GiB cache | 50 GiB cache |
| Availability requirement | Continue after one AZ loss | Continue after one AZ loss | Continue after one AZ loss |
This executable calculation shows why replica counts belong in the budget. The B candidate has four 4-vCPU tasks, two in each AZ:
peak_rps = 100_000 * 50 / 86_400 * 8
backing_reads_s = peak_rps * .90 * (1 - .90)
writes_s = peak_rps * .10
cpu_seconds_s = peak_rps * .008
vcpu_budget = cpu_seconds_s / .50
surviving_vcpu = (4 - 2) * 4
assert round(vcpu_budget, 2) == 7.41
assert round(cpu_seconds_s / surviving_vcpu * 100, 1) == 46.3
# Cold cache raises B's backing reads from 41.7/s to 416.7/s.The same model produces these candidate configurations:
| Derived demand | Illustrative AWS candidate |
|---|---|
| A: 23.1 peak requests/s; 4.63 backing reads/s + 4.63 writes/s; 0.463 vCPU budget. | Two Fargate 1-vCPU/4-GB tasks across two AZs behind an ALB; RDS PostgreSQL db.m7g.large (2 vCPU/8 GiB), 100-GiB gp3, Multi-AZ DB instance. One surviving task has an assumed 23.1% CPU load. |
| B: 463 peak requests/s; 41.7 reads/s + 46.3 writes/s; 7.41 vCPU budget. | Four Fargate 4-vCPU/8-GB tasks, two/AZ behind an ALB; RDS PostgreSQL db.r7g.xlarge (4 vCPU/32 GiB), 500-GiB gp3, Multi-AZ DB instance. Eight surviving app vCPUs have an assumed 46.3% CPU load. |
| C: 6,944 peak requests/s; 312.5 reads/s + 694.4 writes/s; 69.44 vCPU budget. | 27 EC2 m7g.xlarge app nodes, nine/AZ across three AZs behind an ALB; DynamoDB on-demand for distributed keyed access. Eighteen surviving nodes provide 72 vCPU at an assumed 48.2% CPU load. |
For all three, include a replicated cache with at least the stated usable working-set capacity after AZ loss; choose its topology from measured entry overhead, replication behavior and failover needs. These are candidate budgets, not provisioned deployments. A/B assume an orderable supported PostgreSQL version. Their standby provides availability, not extra read throughput; failover interrupts connections. Database memory also serves the engine, connections and maintenance, so a hot set fitting on paper needs measurement.
C assumes whole items ≤1 KiB, including attribute names, eventual nontransactional reads, no secondary indexes and well-distributed keys. Its 8-KiB response includes application-generated content; it does not imply an 8-KiB stored item. Its demand is about 156.25 read units/s and 694.44 write units/s. A 1,025-byte item doubles write units; a 4,097-byte item doubles eventual read units compared with a ≤4-KiB item. DynamoDB's documentation uses binary “KB.” Aggregate response bandwidth is about 455 Mbit/s before overhead.
CPU is only one admission test. Assume B allows 100 live requests/task, 2 MiB extra memory/request and 2,048 MiB baseline process memory. That totals 2,248 MiB within an 8,192-MiB task budget; caches, sidecars and allocator peaks still need accounting. Separately, an assumed database connection budget of 100 with 20 reserved permits four pools of 20, but a rollout surge to six tasks exceeds it:
available_connections = 100 - 20
assert 4 * 20 <= available_connections
assert 6 * 20 > available_connections
# Reduce pool sizes, bound rollout concurrency, or add a measured pooling layer.The fixture rejects invalid inputs and tests cold cache, one-AZ loss, item rounding, memory exhaustion and rollout pool overflow. Its CPU calculation assumes balanced traffic and unchanged CPU cost. Losing an AZ can change cache hits and database work, so these replica counts alone do not establish availability.
Inspect the candidate, then measure it
The optional inspect-aws.sh contains read-only EC2, RDS, ECS and quota inspections. Set the Region and resource identifiers explicitly. For example, this command checks a model's actual published processor, network and EBS fields rather than treating “up to” as a baseline:
aws ec2 describe-instance-types --region us-east-1 \
--instance-types m7g.large m7g.xlarge m8g.48xlarge \
--query 'InstanceTypes[].{Type:InstanceType,vCPU:VCpuInfo.DefaultVCpus,Cores:VCpuInfo.DefaultCores,Threads:VCpuInfo.DefaultThreadsPerCore,MiB:MemoryInfo.SizeInMiB,Network:NetworkInfo.NetworkCards,EBS:EbsInfo.EbsOptimizedInfo}'Inspect RDS orderability for the exact engine version/class; an empty result invalidates that choice. Check task definition CPU/memory/architecture and ECS service deployment platform separately. Quotas describe account permissions to scale, not capacity already running. These commands were checked against CLI documentation and syntax-checked; no account inspection was performed for this lesson.
For each candidate, test peak load, cold cache, a sustained soak and one-AZ loss in an isolated environment. An example acceptance budget is p95 ≤150 ms, p99 ≤300 ms and errors <0.1%; these are chosen goals. Record instance/task, architecture, runtime, endpoint mix, payload, cache hit rate, working set, concurrency, disk block size/queue depth, warmup, duration and sample count. Report offered/completed requests/s, failures and timeouts, endpoint p50/p95/p99, CPU seconds/request, RSS, database waits and storage latency. Never average percentiles or omit failed requests.
Run beyond a short burst window, and ensure the load generator is not saturated. Network/EBS allowances, connection waits and queue growth can explain poor tail latency despite spare CPU. T-family alternatives also require CPU-credit mode and surplus-charge analysis. Start with a falsifiable prediction—“B's cold cache saturates database storage”—then inspect waits, IOPS and latency to see whether it survives the test. Revise the candidate using that evidence.