ShrinkRay All Articles
Video & Media Optimization

Microservices Promised Agility. Nobody Warned You About the Payload Tax.

By ShrinkRay Video & Media Optimization
Microservices Promised Agility. Nobody Warned You About the Payload Tax.

There's a version of the microservices story that gets told at conference talks and in engineering blog posts where everything works out beautifully. Teams deploy independently, services scale in isolation, and the architecture hums along like a well-oiled machine. Then there's the version that plays out in production at 2 a.m., where a single user-facing API call fans out into 12 downstream service requests, each returning its own JSON envelope with its own metadata wrapper, and the aggregated response that finally reaches the client is 4x larger than it needs to be and took 800ms to assemble.

Both versions are real. The second one just gets talked about less.

Where the Weight Comes From

In a traditional monolith, when your application needs to render a user's order history, it fires a couple of SQL queries, assembles the result in memory, and serializes it once. The data takes one shape on the way out the door.

In a microservices architecture, that same operation might involve calls to an Order Service, a Product Catalog Service, a User Profile Service, and a Pricing Service. Each of those services returns its own response — formatted according to its own conventions, wrapped in its own envelope structure, including its own metadata fields. The API gateway or BFF (Backend for Frontend) layer that aggregates these responses has to merge, transform, and re-serialize all of it before sending anything to the client.

Every step in that chain adds bytes. JSON property names are repeated in every object in every array. Timestamp fields appear in four different formats across four services. The id field for a product shows up as product_id in one service, productId in another, and pid in a third — so the aggregation layer keeps all three rather than normalizing, because normalization is a future ticket that never gets prioritized.

This is the microservice payload tax, and it compounds with scale.

The Distributed N+1 Problem

The N+1 query problem is well understood in the context of ORMs and databases: you fetch a list of N records, then make one additional query per record to fetch related data, resulting in N+1 total queries. Microservices create the same pattern at the network level, and it's significantly more expensive.

Consider a product listing page that needs to show 20 products with their current inventory status. The Product Service returns the 20 products in a single call. But inventory lives in the Inventory Service, and the team that built the product page queries it once per product — 20 separate HTTP requests, each with its own TCP handshake overhead, serialization cost, and response envelope. What could have been one batched request becomes 20 individual ones, and the page's time-to-interactive pays for it.

The fix here mirrors the database solution: batch requests wherever possible. Most service APIs can support a ?ids=1,2,3,4 style batch endpoint. If yours don't, that's a conversation worth having with the teams that own those services. The latency savings alone usually make the case.

Redundant Data Marshaling: The Silent Multiplier

Here's something that doesn't show up in architecture diagrams: every time data crosses a service boundary, it gets serialized and deserialized. Service A holds the data as a Go struct. It serializes to JSON to send it across the wire. The API gateway receives that JSON, deserializes it into its own internal representation, maybe transforms some fields, then re-serializes to JSON again to send to the client.

For small payloads, this is negligible. At scale — thousands of requests per second, each touching 5–8 services — the CPU cost of all that marshaling is real and measurable. It shows up as elevated compute costs and latency in your service mesh metrics, often misattributed to "just how distributed systems work."

One practical mitigation is to standardize on a binary serialization format for inter-service communication. Protocol Buffers (protobuf) and MessagePack both offer significantly faster serialization and smaller wire sizes compared to JSON. Services talk to each other in protobuf; only the edge layer translates to JSON for client consumption. You get the efficiency gains internally without breaking the client-facing API contract.

The Aggregation Layer as Payload Factory

API gateways and BFF layers are supposed to reduce client-facing complexity. Done well, they do. Done poorly, they become payload factories — pulling in everything from every upstream service and forwarding it all downstream, leaving it to the client to figure out what it actually needs.

This is where GraphQL often gets proposed as a solution, and it genuinely helps: clients declare exactly what fields they need, and the server returns only those fields. No more over-fetching product objects that include 30 fields when the listing page only renders 5 of them. The tradeoff is resolver complexity and the potential for N+1 problems at the GraphQL layer if the resolvers aren't designed carefully with DataLoader patterns.

For REST-based architectures, sparse fieldsets (a pattern borrowed from the JSON:API specification) give clients similar control. Add a ?fields=id,name,price query parameter, and your aggregation layer filters the response accordingly before serialization. It's less elegant than GraphQL but far easier to retrofit onto an existing API.

Compression as the Last Line of Defense

All of the above is about reducing payload size structurally. But even after you've batched your requests, normalized your fields, and filtered your responses, you should still be applying transport-level compression.

If your microservice responses aren't being gzip or Brotli compressed at the gateway level, you're shipping raw JSON across the wire. For typical API responses — repetitive field names, predictable structures — compression ratios of 5:1 to 10:1 are completely normal. A 50KB response becomes 8KB. That's not a rounding error; that's a material difference in bandwidth cost and client-side parse time.

Brotli, where supported, generally outperforms gzip by 15–25% on text-based payloads. Most modern CDNs and API gateways support it. Check your gateway config and make sure it's enabled.

The Real Lesson

Microservices aren't inherently bloated. The bloat comes from treating service decomposition as purely an organizational and deployment concern while ignoring the data flow consequences. When teams design service boundaries without thinking about how data aggregates at the edges, the payload tax accumulates quietly — one redundant field, one extra service call, one un-batched loop at a time.

The architecture that's supposed to make you faster ends up making your APIs heavier. And unlike the monolith, where you could fix a data shape in one place, distributed bloat requires coordination across teams, services, and contracts. That coordination cost is real — but so is the performance and infrastructure cost of ignoring it.

Measure your API response sizes. Trace your fan-out patterns. Enable compression at the gateway. And the next time someone proposes splitting a service "just to keep things clean," ask what the payload looks like on the other side of that split.