← All guides

Everyone thinks their service needs a C instance

Published October 17, 2026

The compute-optimized family gets picked more often than the workloads justify. “Our service is CPU heavy” is usually a description of what the service does, not of what it uses. Encoding, request routing, compression, JSON parsing, anything that runs on a hot loop sounds compute-bound. Then the team opens CloudWatch a month later and finds the box sitting at 12% CPU with memory at 85%.

C instances have 2 GiB of memory per vCPU, the lowest ratio in the mainstream lineup. Every vCPU you ask for brings a slice of memory with it, and every slice of memory brings a vCPU. That coupling is the whole story. If the service needs 16 GiB, a C family hands it 8 vCPUs whether it can use them or not. The general-purpose M family gives 4 GiB per vCPU, so 16 GiB comes with 4. Same memory, half the vCPUs, lower bill.

What the three families actually cost

us-east-1, Linux, on-demand, 7th generation Graviton:

InstancevCPUMemory (GiB)Price/hrMonthly (730 hrs)
c7g.xlarge (4 vCPU)48$0.1450$105.85
m7g.large (2 vCPU, same 8 GiB)28$0.0816$59.57
m7g.xlarge (4 vCPU)416$0.1632$119.14
r7g.large (2 vCPU, same 16 GiB)216$0.1071$78.18

The row that matters is the second one. Hold memory at 8 GiB, drop from 4 vCPUs to 2, and the price falls 43.7%. The same move at Intel: c7i.xlarge at $0.1785/hr becomes m7i.large at $0.1008/hr, 43.5% cheaper. On AMD, c7a.xlarge at $0.20528 becomes m7a.large at $0.11592, 43.5% cheaper. On the 8th generation, c8g.2xlarge at $0.31904 becomes m8g.xlarge at $0.17952, 43.7% cheaper. The number lands between 43% and 44% on every current generation. It’s the same across architectures, and the same ratio holds in the other regions we checked, because AWS prices vCPU-hours at a fixed rate within a family: halving the vCPUs at constant memory halves the compute portion of the bill. Reserve it for a memory-matched swap and “about 40%” is a fair shorthand.

Keep going down the memory curve and it gets cheaper still. An r7g.2xlarge gives 16 GiB with 4 vCPUs for $0.4284/hr, against $0.6528 for the m7g.4xlarge that has the same 16 vCPUs. That is the mirror image of this guide: M is the right stop for a service that uses half its cores, R is the right stop for a service that uses a quarter of them. For the full curve, see the RAM per dollar post and right-sizing with $/vCPU-hr and $/GiB-hr.

Same generation, same architecture

Every comparison above holds the processor and generation fixed. That is deliberate: a swap from c7i to m7g drops the vCPU count, the memory, and the ability to run the same binaries all at once, and it only makes sense if the workload is already moving to Arm. Read Graviton vs. x86 before you go that far. The C-to-M move doesn’t require a recompile, an AMI rebuild, or a benchmark campaign. Same chip, same instruction set, minus half the cores.

It also depends on a size pair that actually exists. The move only works at xlarge and up, because the M family starts at 2 vCPUs and 8 GiB, which is exactly what a C large already has. On the small end there’s nothing to gain and sometimes a small loss: c7i.large at $0.08925 with 4 GiB is cheaper than the m7i.large at $0.1008 with 8 GiB, and 4 GiB is the least memory either family offers. If the service already runs on a large, it’s near the floor. The money is in 2xlarge and above.

Why the CPU is usually idle

The reason this works so often is that applications are sized for memory and peak bursts, while CPU stays quiet for most of the day. Three patterns cover most of it:

Thread pools and connection handlers get sized for the worst case. A JVM with a 64-thread pool on a single vCPU spends its life context-switching between threads that are waiting on I/O. The pool size is a concurrency setting, not a compute demand.

Garbage collectors and interpreters use one or two cores hard and the rest barely at all. A Node.js service is single-threaded by design. A Python service is bound by the GIL. Both of them happily run on 2 vCPUs and will not go faster on 8.

In-memory data holds the memory, not the CPU. A cache node, a search shard, a session store, a queue backed by Redis all size themselves by dataset. The working set decides the memory line on the instance, and the request rate decides the CPU line, and the two rarely move together.

The tell is a CPU utilization graph that never leaves the 5 to 25% band even during peak, paired with a memory graph that sits high and steady. If you have that, you’re paying for cores the kernel never schedules.

Checking before you move

You need two numbers, both from a week that includes your busiest day:

  1. Peak vCPU usage, not average. Take the maximum of CPUUtilization over the window, divide by 100, multiply by the vCPU count. That’s the number of vCPUs actually alive at the worst moment. On htop or top, the load average over 15 minutes is a decent proxy if you divide it by vCPUs and keep it under 0.7.
  2. Memory high-water mark, from mem_used_percent on the CloudWatch agent, not the OS’s reported free memory, which subtracts page cache.

The rule for this move: peak vCPU usage at or below half the current count, and memory high-water at or below the memory you’d keep. A c7i.2xlarge (8 vCPU, 16 GiB) running 3.5 vCPUs at peak, with memory at 15 GiB, moves to m7i.xlarge (4 vCPU, 16 GiB) with room to spare. One running 6 vCPUs at peak does not, and should stay on C or move to a larger M.

Two things to watch before you flip the switch. Bursty services with rare spikes, nightly batch runs, cron fan-outs, and image resizes, need their peak measured over the same window as the batch, not over a weekday afternoon. And note that the memory line has to stay flat: a service that grows its heap slowly will run out of memory on the M instance before it runs out of CPU, and the OOM kill will not be gentle. Check the memory trend, not just the ceiling.

Network bandwidth is the other constraint, and it’s easy to check: the M instance at half the vCPUs usually has the same network ceiling as the C instance it replaces, because network scales with size in both families and the two halves of a pair land on the same tier. EBS bandwidth per instance matches too. What changes is IOPS and throughput per vCPU. Look at both rows on the comparison tool if the service is network-heavy, since the two sizes differ at some generations.

What still belongs on C

Plenty of things do. Video encoding and transcoding at a fixed throughput, batch ETL that runs hot for hours, build agents that compile large codebases, game servers, anything with a hard real-time loop, and any service where CPU utilization legitimately sits above 60% at peak. The test isn’t the service category, it’s whether the cores are scheduled. A Node build farm on C instances is correct. A Node API server on C instances usually isn’t, for the same reason.

The other case for C is the memory ratio itself. A service that needs 16 GiB and 6 vCPUs fits a c7g.2xlarge (8 vCPU, 16 GiB). Putting it on M means either 16 GiB at 4 vCPUs, which undershoots, or 32 GiB at 8, which adds memory it won’t use. Do the arithmetic on both numbers before assuming C is the problem.

The typical fleet

Most accounts have one application where this applies. Start with your top instances by monthly cost, since those carry the most money even when the percentage saved is the same, and pull CPU and memory for the busiest week. The candidates have a memory high-water above 60% and a peak CPU below 25%.

Moving a single c7g.2xlarge to m7g.xlarge saves $83.22 a month. Across three such instances that’s about $3,000 a year, from a change nobody has to deploy. The instance explorer and comparison tool will price any pair you’re considering, in your region and OS, before you touch production.