Skip to main content
the invisible-layer how abstraction is making software engineers dumber

The Performance Illusion

11 min read Chapter 32 of 56
Summary

Argues that cloud auto-scaling has made bad code...

Argues that cloud auto-scaling has made bad code economically viable but environmentally and financially costly, presents case studies where understanding one layer below saved millions, and introduces profile-driven optimization as the antidote to scaling-as-thinking.

The Performance Illusion

The cloud made bad code viable. That is both its greatest feature and its most expensive consequence.

Before elastic infrastructure, performance problems had sharp edges. Your application ran on four servers. When it slowed down, you felt it immediately — users complained, pages timed out, the CEO called. You had no option except to make the code faster. You profiled, you optimized, you learned what the machine was actually doing with your instructions.

Today, when your application slows down, an auto-scaler spins up more instances. Response times stay flat. Users notice nothing. Neither do you. The only signal is the AWS bill, and by the time finance flags it, the slow code has been in production for months and nobody remembers who wrote it or why.

We’ve replaced performance engineering with performance purchasing. And the invoice is staggering.

The Auto-Scaler as Performance Band-Aid

Here’s a service I observed in production at a mid-size SaaS company. Their product catalog API served a JSON response for each product page. Average response time: 340ms. Traffic: 2,000 requests per second at peak. Running on c5.xlarge instances (4 vCPU, 8GB RAM) behind an Application Load Balancer.

At 2,000 RPS with a 340ms average response time, each instance could handle roughly 12 concurrent requests at steady state. The auto-scaler maintained enough instances to keep CPU below 60%. That worked out to 28 instances during peak hours, scaling down to 8 during off-peak.

Monthly compute cost: $38,400 (28 instances × 12 peak hours + 8 instances × 12 off-peak hours × 30 days × $0.17/hour, plus the ALB, data transfer, and CloudWatch).

An engineer profiled the endpoint during a quiet sprint. The flamegraph revealed that 62% of the response time — 211ms of the 340ms — was spent in JSON serialization. The product data was loaded from a precomputed cache in 18ms. The business logic took 14ms. Serializing the response object to JSON took 211ms.

Why? The product object had 847 fields, most of them nested, and the serializer was using reflection to discover the object’s structure on every single request. There was no schema. There was no precompilation. Every request paid the full cost of introspecting a deeply nested object graph and converting it to text.

The fix took three days: switch from the reflection-based serializer to a schema-compiled serializer (in this case, moving from json.dumps() on large nested dictionaries to a Pydantic model with compiled serialization). Response time dropped from 340ms to 87ms. The same throughput now required 8 instances at peak instead of 28.

New monthly compute cost: $8,200. Annual savings: $362,400.

Nobody noticed the performance problem for fourteen months because the auto-scaler kept response times acceptable. The cloud bill grew gradually — $27K, $30K, $33K, $38K — and each monthly increase was small enough to attribute to traffic growth. The code was never the suspect because the symptoms were hidden.

The Cloud Cost Iceberg

Cloud cost iceberg showing visible and hidden costs

What makes cloud cost optimization uniquely hard is that the waste is distributed across hundreds of services, and each service’s waste looks small in isolation. A single endpoint that’s 4x slower than it should be costs an extra $2,000 per month. Across fifty endpoints and twenty services, that’s $2 million per year — but no single line item exceeds the threshold that triggers investigation.

The iceberg metaphor is precise. Above the waterline: the compute costs you can see on the billing dashboard. Below the waterline:

  • Data transfer costs amplified by verbose serialization (JSON responses that are 3x larger than necessary because nobody trimmed the payload)
  • Storage costs from logs generated by inefficient retry loops (each failed attempt logged at INFO level, 5 retries per failure, millions of failures per day)
  • Database costs from unoptimized queries that force the database to scale to a larger instance class
  • CDN costs from cache miss rates caused by non-deterministic response generation (same request produces different headers, CDN treats each as unique)
  • Engineering time spent managing the complexity of a system that’s 4x larger than it needs to be — more instances means more deployment targets, more monitoring, more failure modes

Every inefficiency has a multiplier. Slow code doesn’t just cost CPU. It costs network, memory, storage, and human attention, compounded across every hour the system runs.

Case Study: The N+1 Query That Cost $480,000

A fintech company’s transaction history endpoint was their most-called API — 15 million requests per day. Each request loaded a user’s recent transactions. The endpoint worked perfectly.

It also made 47 database queries per request.

The initial query fetched the transaction list. Then, for each transaction, a separate query fetched the merchant details. Another query fetched the category. Another fetched the fee schedule. This is the classic N+1 query pattern: one query for the list, N queries for each item’s related data.

The database was an RDS PostgreSQL instance running on db.r5.4xlarge — 16 vCPUs, 128GB RAM — to handle the query volume. The queries were individually fast (2-3ms each), so no single query appeared in the slow query log. But 47 queries × 2ms = 94ms of database time per request, plus the overhead of 47 connection round-trips.

At 15 million requests per day, that’s 705 million database queries per day. The database instance cost $2,700/month. But the real cost was the four read replicas needed to distribute the load: another $10,800/month. Plus the application instances needed to wait for the sequential database calls, which inflated response time and required more horizontal scaling: $8,400/month in additional compute. Plus the network transfer between application and database: $1,500/month. Plus the enhanced monitoring and Performance Insights to keep track of the overloaded database: $600/month.

Total attributable cost: $24,000/month for one endpoint.

The fix was a single SQL query with JOINs and a judicious use of json_agg() to return nested data in one round-trip. The endpoint went from 47 queries averaging 94ms of database time to 1 query averaging 12ms. The database instance was downsized to db.r5.xlarge. Three of the four read replicas were decommissioned. Application instances scaled down proportionally.

New cost: $4,100/month. Annual savings: $238,800. Over the two years this pattern had been in production: $477,600 in unnecessary spending.

One engineer, one week of work, one SQL query. The ROI is difficult to overstate.

Case Study: The GC Pressure That Nobody Measured

A Java-based trading platform processed market data feeds — millions of events per second through a pipeline of enrichment, validation, and routing services. The system worked, but the tail latency was brutal: p99 at 45ms, p99.9 at 340ms. For a trading system, 340ms might as well be infinity.

The team’s first instinct was to throw hardware at it. They upgraded from c5.2xlarge to c5.4xlarge instances, doubling CPU and memory. Tail latency improved marginally: p99.9 dropped from 340ms to 280ms. They added more instances. Marginal improvement. They investigated network latency, switched to placement groups, tried enhanced networking. Marginal improvement.

Total additional infrastructure cost: $168,000/month in compute upgrades and additional instances.

Finally, someone profiled the GC. The JVM was running G1GC with default settings and a 16GB heap. The GC logs showed mixed collection pauses of 150-300ms every 8-12 seconds. Every p99.9 latency spike aligned perfectly with a GC pause.

The root cause: the enrichment service was creating millions of short-lived HashMap objects per second for intermediate computation results. Each event created a new HashMap, populated it, read from it once, and discarded it. The objects were tiny (5-10 entries each) but created at a rate that filled the young generation in seconds. When the young generation filled, the JVM promoted objects to the old generation, which eventually triggered mixed collections with stop-the-world pauses.

The fix had two parts:

  1. Replace per-event HashMap creation with object pooling, reusing a thread-local map that was cleared between events instead of allocated and garbage-collected.
  2. Tune G1GC: reduce MaxGCPauseMillis to 10ms, increase the young generation ratio, and switch long-lived reference data to off-heap storage using direct byte buffers.

After the fix: p99 dropped to 4ms. p99.9 dropped to 11ms. The team reverted to the original c5.2xlarge instances and reduced the instance count because each instance could handle 3x the throughput without GC-induced stalls.

Savings: $2.1 million per year in compute costs, plus the trading advantage of sub-millisecond processing that the old architecture couldn’t achieve at any price.

The engineer who found this had one skill the rest of the team lacked: they understood JVM garbage collection. Not as a theoretical concept — as a system with specific behaviors, tunable parameters, and observable outputs. They knew what to measure (GC logs, allocation rate, promotion rate) and what the numbers meant. The observability tools had been collecting GC metrics the entire time. Nobody had looked at them because nobody knew they mattered.

The Environmental Invoice

Here’s a cost that doesn’t show up on the AWS bill.

Data centers consumed approximately 460 terawatt-hours of electricity in 2024 — roughly 2% of global electricity production, comparable to the entire country of France. By 2030, projections suggest this will reach 1,000 TWh.

How much of that energy is wasted on inefficient code?

It’s impossible to measure precisely, but consider the signals. The product catalog service from the first example was consuming 3.5x more compute than necessary. The fintech endpoint was generating 47x more database queries than necessary. The trading platform was allocating and garbage-collecting orders of magnitude more objects than necessary.

If these examples are representative — and industry surveys suggest they are, with most cloud workloads running at 10-20% efficiency — then somewhere between 30% and 60% of data center energy consumption is processing instructions that serve no purpose. That’s 140-280 TWh per year of wasted electricity. At the global average carbon intensity of electricity generation, that’s 70-140 million metric tons of CO₂ per year attributable to software inefficiency.

For context, that’s roughly equivalent to the total annual emissions of the Netherlands.

We’re burning fossil fuels to execute retry loops around unoptimized JSON serializers inside auto-scaling groups that exist because nobody profiled the hot path. The cloud makes this invisible — you don’t see the power plant, the cooling system, the diesel generators for backup power. You see a monthly bill denominated in dollars, and the dollars are cheaper than an engineer’s time.

Until they’re not. Until the bill reaches the point where CFOs start asking questions. Or until carbon regulations make the energy cost explicit. Or until your competitors, who actually profiled their code, undercut you — not through better features, but through lower operating costs that allow them to price aggressively while remaining profitable.

Profile-Driven Optimization

The antidote to scaling-as-thinking is measurement-first optimization. Before you change a single line of code, measure where the time goes.

Python:

py-spy record -o profile.svg --pid 12345
# Or for a specific command:
py-spy record -o profile.svg -- python my_service.py

Java:

# Using async-profiler
./asprof -d 30 -f profile.html <pid>

Go:

import _ "net/http/pprof"
// Then: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

General (Linux perf):

perf record -g -p <pid> -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > profile.svg

The flamegraph tells you exactly where your CPU time is spent. Not where you think it is — where it actually is. In my experience, engineers’ intuitions about performance bottlenecks are wrong more than 70% of the time. The function you’re sure is slow is fast. The function you’ve never looked at is consuming half your CPU.

This is why profiling must precede optimization, always. Optimizing without profiling is performing surgery without an X-ray. You might improve something. You probably won’t improve the right thing. And you might make something else worse.

The Performance Ethic

The cloud is one of the most important infrastructure innovations in computing history. Elastic scaling, managed services, global distribution — these capabilities enable applications that were impossible to build a decade ago.

But the cloud also enables a kind of laziness that has real costs — in dollars, in energy, in engineering skill. When you can buy your way out of every performance problem, you stop learning how to solve them. When you stop learning, the problems compound. When the problems compound, the costs compound. And eventually, you’re running 200 instances of a service that could run on 20, paying $40,000 a month for compute that should cost $8,000, and wondering why your margins are thinner than your competitors’.

The fix isn’t to leave the cloud. The fix is to understand what’s running on it.

Profile before you scale. Measure before you optimize. Understand before you provision. The engineer who does this saves their company money, reduces the industry’s environmental footprint, and — not incidentally — becomes dramatically more valuable than the one who knows only how to drag a slider from 10 instances to 50.

The cloud is a tool. Like all tools, it amplifies what you bring to it. Bring understanding, and it amplifies your effectiveness. Bring ignorance, and it amplifies your costs.