- GPU is 5x faster than CPU for single-request inference (8ms vs 42ms on BERT), but costs 3x more per hour — overkill for traffic under 10k requests/day.
- CPU wins for bursty, low-traffic workloads (<10k req/day, >100ms SLA); GPU wins at >50k req/day sustained traffic or <50ms p95 latency requirements.
- Dynamic batching (batch size 16) boosts GPU throughput to 412 req/sec vs CPU's 89 req/sec, but adds 20-40ms queuing delay — test against your SLA before deploying.
- Hidden GPU costs include CUDA driver compatibility issues, 3GB+ Docker images, 60-90s autoscaling lag, and float16 overflow bugs that don't appear on CPU.
- For side projects and MVPs, stick with CPU and ONNX Runtime optimizations (ORT_ENABLE_ALL cuts latency 40%); GPU pays off only when latency headroom or high throughput justifies the operational overhead.
The $400/Month Surprise
I ran the same BERT model on a T4 GPU and a 4-core CPU for a month. The GPU was faster, obviously. But it cost $400 more than the CPU setup, which handled 95% of requests under our 200ms SLA just fine.
Most benchmarks compare raw throughput or single-request latency. They skip the part where you actually pick hardware for a production system with a budget, an SLA, and real traffic patterns. This post runs five realistic scenarios — from a personal side project to a high-traffic API — and shows when GPUs pay for themselves and when you’re just burning money.

Test Setup: Models, Hardware, Traffic
I tested three model sizes across two compute tiers:
Models:
– BERT-base (110M params): text classification, sequence length 128
– ResNet50 (25M params): image classification, 224×224 input
– Whisper-tiny (39M params): speech-to-text, 30s audio clips
Hardware:
– GPU: AWS g4dn.xlarge (NVIDIA T4, 16GB VRAM, 4 vCPUs) — $0.526/hour on-demand
– CPU: AWS c5.xlarge (4 vCPUs, 8GB RAM, Intel Xeon Platinum 8124M) — $0.17/hour on-demand
Both ran Ubuntu 22.04, PyTorch 2.1.0, ONNX Runtime 1.16.3. I used ONNX exports for fair comparison (PyTorch eager mode penalizes CPU too much). The GPU ran with TensorRT optimization where applicable; CPU used OpenVINO-style graph optimizations in ONNX Runtime.
Traffic patterns:
– Scenario 1: 10 req/day (personal tool)
– Scenario 2: 500 req/day (MVP side project)
– Scenario 3: 5000 req/day, spiky (B2B SaaS, 9am-5pm peak)
– Scenario 4: 20000 req/day, steady (consumer app)
– Scenario 5: 100000 req/day, real-time SLA <100ms (high-traffic API)
All tests measured p50, p95, p99 latency under load using Locust with realistic request spacing.
Latency: Where GPUs Actually Win
Here’s BERT-base inference latency (batch size 1, single request):
| Hardware | p50 | p95 | p99 |
|---|---|---|---|
| T4 GPU | 8ms | 11ms | 14ms |
| CPU (4 cores) | 42ms | 58ms | 71ms |
GPU is 5x faster. But does that matter for your use case?
For Scenario 1 (10 req/day), you’re serving one request every 2.4 hours. A 50ms response vs 10ms is imperceptible to the user, and the server sits idle 99.99% of the time anyway.
For Scenario 5 (100k req/day = 1.16 req/sec sustained, peaks ~10 req/sec), the math changes. At 42ms CPU latency, a single core handles $1 / 0.042 = 23.8 req/sec on a single thread, leaving headroom for spikes.
But there’s a catch: batch size.
GPUs amortize overhead across batched requests. If you can queue 8-16 requests and process them together, GPU throughput explodes. CPU gains much less from batching (memory bandwidth is the bottleneck, not parallelism).
Here’s BERT-base throughput with dynamic batching (max batch size 16, 100ms wait window):
| Hardware | Throughput (req/sec) | p95 latency |
|---|---|---|
| T4 GPU | 412 | 38ms |
| CPU (4 cores) | 89 | 127ms |
The GPU is now 4.6x faster in throughput, but p95 latency increased due to batching delay. If your SLA is <50ms and you can’t batch, the GPU’s single-request latency (11ms p95) is your only option. If you can tolerate 100-150ms and batch aggressively, the CPU might still hit your target at 1/3 the cost.
Cost Breakdown: Monthly Bill Reality
Let’s price out Scenario 3 (5000 req/day, 9am-5pm traffic).
Assumptions:
– 8-hour active window, traffic drops to ~5% overnight
– Autoscaling with 2-minute warmup (can’t scale instantly)
– Target: p95 < 200ms
CPU setup:
– c5.xlarge handles ~80 req/sec with batching, p95 ~130ms
– Peak traffic: 5000 req / 8 hours = 0.17 req/sec average, spikes to ~2 req/sec
– Single instance suffices; run 12 hours/day to cover peak + tail traffic
– Cost: $0.17/hour × 12 hours × 30 days = $61.20/month
GPU setup:
– g4dn.xlarge handles ~400 req/sec, p95 ~40ms
– Massive overkill for this traffic, but you can’t buy a fraction of a GPU
– Run 10 hours/day (peak + buffer)
– Cost: $0.526/hour × 10 hours × 30 days = $157.80/month
The GPU is $4000/month more expensive for a workload that doesn’t need it. You’re paying for 400 req/sec capacity when you peak at 2 req/sec.
Now Scenario 5 (100k req/day = sustained 1.16 req/sec, peaks ~15 req/sec):
CPU setup:
– Need 15 / 80 = 0.19 instances at peak → round up to 1 instance, but p95 spikes to 180ms under load
– To stay under 100ms p95, need 2× c5.xlarge (160 req/sec combined)
– Run 24/7 (traffic is global, no downtime)
– Cost: $4001/hour × 2 instances × 24 hours × 30 days = $4002/month
GPU setup:
– Single g4dn.xlarge handles 15 req/sec easily, p95 ~40ms
– Run 24/7
– Cost: $4003/hour × 24 hours × 30 days = $4004/month
GPU is $4005/month more, but you get 40ms p95 vs 100ms. If your SLA is <100ms, the CPU setup is on the edge — one traffic spike and you’re breaching. The GPU has 10x headroom.
Here’s the cost table for all five scenarios:
| Scenario | Requests/day | CPU cost/month | GPU cost/month | Winner |
|---|---|---|---|---|
| 1 (personal) | 10 | $4006 | $4007 | CPU |
| 2 (MVP) | 500 | $4008 | $4009 | CPU |
| 3 (B2B) | 5000 | $0.5260 | $0.5261 | CPU |
| 4 (consumer) | 20000 | $0.5262 | $0.5263 | CPU |
| 5 (high-traffic) | 100000 | $0.5264 | $0.5265 | GPU* |
*GPU wins Scenario 5 only if your SLA is <100ms AND you need the latency headroom. If 150ms is acceptable, CPU is still cheaper.
When CPU Beats GPU: The Under-1000-Requests Rule
For most side projects and MVPs, CPU inference is the right call. Here’s why:
-
Idle cost kills you. GPUs don’t scale to zero gracefully. AWS doesn’t sell sub-hour billing for GPU instances (you pay for the full hour). If your traffic is bursty — 100 requests at 9am, nothing until 2pm — you’re paying for idle GPU time. I found that below ~1000 requests/day, the GPU utilization stays under 5%, meaning you’re burning $0.5266+/month for hardware that’s asleep.
-
Cold start overhead. CUDA initialization adds 2-4 seconds on first request. If you’re using serverless (Lambda, Cloud Run), GPUs are a non-starter unless you keep instances warm (). CPU cold starts are <500ms.
3. **Model size matters less than you think.** I tested Whisper-tiny (39M params) on CPU and hit 340ms p95 latency for 30-second audio clips. Totally fine for asynchronous transcription (user uploads, gets result in 1-2 seconds including queue time). The GPU did it in 85ms, but the user experience is identical.
4. **ONNX Runtime is shockingly good.** With graph optimizations enabled (<!–MD_CODE_BLOCK_1–>), CPU inference on BERT dropped from 68ms to 42ms. The GPU didn't benefit as much (10ms → 8ms). At small batch sizes, ONNX on CPU closes the gap.
And if you're just prototyping, spinning up a DOLLAR_AMOUNT_27/month CPU instance beats configuring CUDA drivers.
<figure class="stock-image">
<img src="https://tildalice.io/wp-content/uploads/2026/04/stock-gpu-vs-cpu-inference-cost-latency-benchmark-2.jpg" alt="Detailed view of a GeForce RTX graphics card, highlighting modern technology." loading="lazy" />
<figcaption>Photo by <a href="https://www.pexels.com/@bertellifotografia" rel="nofollow">Matheus Bertelli</a> on <a href="https://www.pexels.com" rel="nofollow">Pexels</a></figcaption>
</figure>## When GPU Pays Off: Batching, Latency, Scale
GPUs win when:
**1. You can batch requests.** Real-time APIs often can't (each user wants their result NOW), but asynchronous workflows (video processing, document analysis, nightly ETL) can queue up 32-128 inputs and blast through them. I tested ResNet50 image classification at batch size 64:
– **CPU**: 2.1 seconds per batch → 30 images/sec
– **GPU**: 0.18 seconds per batch → 355 images/secThe GPU is 11.8x faster. If you're processing 100k images/day, that's 55 minutes on GPU vs 9.3 hours on CPU. The GPU instance runs 1 hour/day (DOLLAR_AMOUNT_28), the CPU needs to run continuously or scale horizontally. At that volume, GPU is cheaper.
**2. Your SLA is <50ms p95.** CPU struggles to go below 40ms for transformer models. If you're building a chatbot, autocomplete API, or real-time recommendation engine, the GPU's 8-15ms latency is the only way to hit <50ms end-to-end (after network, serialization, etc.).
**3. You're hitting >10k requests/day with spiky traffic.** Autoscaling CPUs works, but you need 2-5 minutes to spin up new instances (ECS/Kubernetes pod scheduling + model load time). GPUs handle spikes with spare capacity. For Scenario 5, a single GPU absorbed 3x traffic spikes without breaking a sweat. The CPU setup needed 3 instances during peaks, which meant paying for 2 idle instances overnight or accepting SLA breaches.
**4. You're memory-bound, not compute-bound.** Large models (GPT-2, LLaMA-7B, Stable Diffusion) don't fit in CPU RAM efficiently. A 7B parameter model needs ~14GB in float16, which eats your entire c5.xlarge memory. The T4 has 16GB VRAM dedicated to inference. (Though at that scale, you'd use A10G or A100, not T4.)
## The Code: CPU vs GPU Inference Harness
Here's the test harness I used for BERT. It loads ONNX models, runs inference, and logs latency.
<!–MD_CODE_BLOCK_0–>
One gotcha: ONNX Runtime's CPU backend benefits heavily from <!–MD_CODE_BLOCK_2–> graph optimization. Without it, I saw 68ms latency; with it, 42ms. The GPU saw minimal improvement (10ms → 8ms) because most optimizations target CPU-specific operations.
Another: the warmup loop is critical for GPU benchmarks. CUDA kernels compile lazily on first execution, so the first inference takes 80-200ms. If you skip warmup, your p99 looks terrible.
## Batch Size Sweet Spot: Throughput vs Latency Trade-off
Dynamic batching queues requests for up to <!–MD_CODE_BLOCK_3–>, then processes them together. Larger batches = higher throughput, but also higher latency (requests wait in queue).
I tested BERT with max batch sizes 1, 4, 8, 16, 32 and a 100ms wait window:
| Batch size | GPU throughput (req/s) | GPU p95 latency | CPU throughput (req/s) | CPU p95 latency |
|————|———————-|—————-|———————-|—————-|
| 1 (no batching) | 125 | 11ms | 24 | 58ms |
| 4 | 285 | 28ms | 52 | 98ms |
| 8 | 368 | 34ms | 71 | 119ms |
| 16 | 412 | 38ms | 89 | 127ms |
| 32 | 438 | 45ms | 102 | 156ms |GPU throughput scales almost linearly up to batch size 16, then flattens (memory bandwidth ceiling). CPU throughput grows slower and hits a wall at batch size 32 (cache thrashing).
If your SLA is <50ms p95, you can batch up to size 8 on GPU (34ms p95), but CPU breaches at batch size 4. If your SLA is <150ms, CPU can batch up to size 16 and hit 89 req/sec — good enough for Scenario 3-4.
The formula for required capacity:
\text{instances} = \lceil \frac{\text{peak_req_per_sec}}{\text{throughput_per_instance}} \rceil
$$
For Scenario 5 (peak 15 req/sec), GPU needs instance. CPU needs instance if you batch aggressively, but p95 latency spikes to 127ms under load. To stay under 100ms, you’d size for instance at batch size 8, but that only gives 71 req/sec — barely enough. One traffic spike and you breach.
This is why GPU wins at high traffic: headroom.
Hidden Costs: CUDA Tax, Driver Hell, Debugging
GPUs aren’t just more expensive per hour — they add operational overhead:
1. Driver compatibility matrix. PyTorch 2.1 needs CUDA 11.8 or 12.1. ONNX Runtime GPU builds are tied to specific CUDA versions. I wasted 3 hours debugging “CUDA driver version is insufficient” because the AMI had CUDA 12.2, but the ONNX Runtime wheel was built for 11.8. The fix: downgrade CUDA or compile ONNX Runtime from source (neither is fun). CPU just works.
2. Docker image bloat. My CPU inference image: 890MB. GPU image with CUDA runtime: 3.2GB. Longer pull times, higher ECR storage costs, slower deploys.
3. Autoscaling lag. Spinning up a GPU instance takes 60-90 seconds (CUDA initialization + model load). CPU instances are ready in 20 seconds. If you’re using Kubernetes HPA, set --horizontal-pod-autoscaler-upscale-delay=30s for CPU, but 90s+ for GPU or you’ll get crashloops.
4. Debugging on CPU, failing on GPU. I hit a fun bug where BERT inference worked fine on CPU but returned NaN on GPU. The culprit: a float16 overflow in one attention head. ONNX Runtime defaults to float32 on CPU but float16 on GPU for speed. The fix: force float32 everywhere, accept the 10% slowdown. This kind of thing doesn’t happen on CPU.
If you’re a solo dev or a small team, these add up. You’re not just paying $0.5269/month for the instance — you’re paying in debugging time, deploy complexity, and DevOps headaches. For a side project, that’s a dealbreaker.
Real-World Recommendation: What I’d Pick
Here’s my decision tree:
Use CPU if:
– <10k requests/day
– SLA is >100ms p95
– Traffic is bursty (not 24/7)
– You’re prototyping or running a side project
– Your model is <500M params and fits in 8GB RAM
– You value simplicity over raw speed
Use GPU if:
– >50k requests/day sustained
– SLA is <50ms p95
– You can batch aggressively (asynchronous workflows)
– Your model is >1B params (LLMs, diffusion models)
– Cost is less important than latency
– You have DevOps support for CUDA environments
For everything in between (10k-50k req/day, 50-100ms SLA), it depends on your traffic pattern. If it’s spiky, GPU gives you headroom. If it’s steady, CPU with autoscaling is cheaper.
One scenario I didn’t test but would change the math: spot instances. AWS g4dn.xlarge spot pricing is ~$0.170/hour (70% off on-demand). If your workload tolerates interruptions (batch jobs, asynchronous processing), GPU spot instances are cheaper than on-demand CPU. I’d use that for Scenario 4-5 if the app can handle occasional pod evictions.
Another angle: serverless. AWS Lambda now supports GPU (sort of — via SageMaker Serverless Inference). But the cold start is brutal (4-8 seconds), and you pay per inference + idle time. For <1000 req/day, it might beat keeping an instance warm. I haven’t tested it thoroughly, so take that with a grain of salt.
What I’m Still Figuring Out
I didn’t test multi-model serving (running BERT + ResNet + Whisper on the same instance). Triton Inference Server and TorchServe both claim to handle this efficiently, but I suspect GPU memory fragmentation becomes an issue. If you’re serving 5 models, do you need 5× the VRAM, or can you share? I’m not entirely sure. I covered some TorchServe issues here, but multi-model is a different beast.
I also didn’t test ARM instances (Graviton3). AWS c7g instances are 20% cheaper than x86 and supposedly handle int8 inference well. If you quantize your model to int8 (which ONNX Runtime supports), Graviton might beat both x86 CPU and GPU on cost/performance. But I don’t have hard numbers yet.
Finally: model quantization. I ran everything in float32 (CPU) and mixed precision (GPU). Int8 quantization can cut latency by 2-3x on CPU with minimal accuracy loss. If you quantize BERT to int8, does CPU latency drop to 15ms and eliminate the GPU advantage entirely? That’s worth testing. Running quantized models means you might not need a GPU at all — or you could run a smaller, cheaper GPU. I need to benchmark this properly before making a call.
FAQ
Q: Does TensorRT really make a difference, or is ONNX Runtime good enough?
TensorRT cut GPU latency from 11ms to 8ms for BERT (27% improvement). For ResNet50, it went from 6ms to 3.5ms (42% improvement). The bigger the model, the more TensorRT helps. But it adds another dependency (CUDA, cuDNN, TensorRT versions must all align), and the Python API is clunky. ONNX Runtime is easier to deploy and “good enough” for most cases. If you’re hitting >100k req/day and latency is critical, TensorRT is worth the pain.
Q: What about TPUs or other accelerators?
I didn’t test TPUs (Google Cloud TPU v4) or AWS Inferentia. TPUs are optimized for large batch sizes (64+) and huge models (GPT-scale). For small models like BERT, the overhead isn’t worth it. Inferentia is cheaper than GPU ($0.171/hour for inf1.xlarge vs $0.172 for g4dn.xlarge), but you need to compile models with AWS Neuron SDK, which only supports TensorFlow and PyTorch (no ONNX). If you’re all-in on AWS and willing to rewrite your inference pipeline, Inferentia might beat both CPU and GPU on cost. I haven’t tested it because the Neuron SDK documentation is… sparse.
Q: Can I just use a serverless GPU and avoid paying for idle time?
In theory, yes. In practice, GPU cold starts are 3-6 seconds, so your first request is painfully slow unless you keep instances warm (which costs almost as much as running 24/7). SageMaker Serverless Inference charges $0.173/hour for warm instances + $0.174 per inference. If you’re doing <5000 inferences/month, it’s cheaper than a full instance. Above that, you’re better off with a dedicated GPU. Modal and Banana.dev claim sub-1s GPU cold starts, but I haven’t tested them in production.
Stress-testing infrastructure at 2am? Grab some Liquid I.V. Hydration Packs — because coffee only gets you so far before the jitters kick in.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,956 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (1,056 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (983 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (905 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (707 views)