Benjamin Cane
Portrait of Benjamin Cane
Benjamin Cane
September 2, 2026
performance
a bunch of blue wires connected to each other
Photo by Scott Rodgerson on Unsplash

At what point does better performance stop being worth it?

When thinking about performance, especially for low-latency and high-scale applications, I don’t believe in “good is good enough.” I think it’s important to leave yourself room to grow and keep looking for improvements.

But there is a point where gaining performance requires significantly more complexity. And sometimes that tradeoff isn’t worth it.

I recently had a conversation with a team considering gRPC to send events from one system to another.

We are talking about very high volumes of relatively small requests.

They had two important requirements:

  • Events needed to move quickly
  • The sender needed to know each event was successfully received

Given their other constraints, traditional HTTP APIs and message brokers weren’t a great fit.

But I posed an interesting question: should they use unary gRPC calls or bidirectional streams?

🚀 The Faster Option

Their initial plan was to implement bidirectional streaming.

The client could continuously send events across a stream, and the server could asynchronously send acknowledgments as it accepted events. It’s not a bad design.

From a performance perspective, it’s a great design.

But there is a catch.

With this approach, the application needs to track what was sent, what is still in-flight, what was acknowledged, and what needs to be retried. And that’s if all goes well. What happens if the stream disconnects? Which events were processed, and which were lost? How do you avoid duplicates?

All solvable problems, but they all add complexity to the application.

🤔 What If You Don’t Need The Fastest Option?

Now let’s compare with unary gRPC.

From an application perspective, you send a request, and you get a response. Success or failure is tied directly to that request.

You don’t need an in-flight cache mapping acknowledgments back to events because the request/response lifecycle already does that for you.

It’s a much simpler implementation. And importantly, unary gRPC is still fast.

You still get the benefits of gRPC: Protocol Buffers, persistent HTTP/2 connections, and multiplexing requests across those connections. But you do give up some performance compared with a well-designed streaming implementation.

But the real question is, did they need that additional performance?

⚖️ There Are Also Operational Tradeoffs

Streaming changes how traffic is distributed. A long-lived stream is established with a particular backend and remains there for the life of that connection.

If you have three clients, six servers, and each client opens one stream, you could end up actively sending traffic to only three servers or less.

It’s solvable: open multiple streams, build a stream pool, balance the work across them, and manage the connection lifecycle during failures. Solvable, but more complexity for the team to implement.

In contrast, with unary requests, infrastructure like a service mesh can distribute requests across available backends without the application needing to manage pools, connection lifecycles, etc.

Send a request to the service mesh; it will distribute it across the six servers. Scaling and failure handling become far less complex.

📈 Performance Has Diminishing Returns

The team will test unary operations for their application.

If unary operations meet their throughput and latency requirements with plenty of headroom for growth, they face an interesting choice.

Bidirectional streaming could give them even more performance. But what are they buying with that additional performance?

If they need it, the complexity is justified. If they don’t, they are taking on additional development, testing, and operational complexity for performance they won't use.

That’s a bad tradeoff.

🧐 Final Thoughts

I dislike the “good is good enough” mentality. Continuously improving your platforms is an essential practice for any platform that cares about performance.

But performance has a cost. Few of us are building systems just to be fast. We’re also building them to be reliable and maintainable. And eventually, someone has to operate that system in the middle of the night when something unexpected happens.

If the simpler solution meets your needs while leaving enough headroom to scale for tomorrow, it may be the best option. The question to ask yourself is: what is the right balance of simplicity and performance?

Discuss on LinkedIn Newsletter Back to all posts

More to Read

  • August 26, 2026 Sometimes good engineering looks like over-engineering reliability
  • August 19, 2026 AI makes code cheap to create, not cheap to own agents
  • August 13, 2026 “We can’t run locally” is usually a design smell engineering
  • August 6, 2026 To make a service more stable, eliminate dependencies reliability
  • July 30, 2026 Caching isn’t hard. Some data is hard to cache performance

Practical engineering notes by Benjamin Cane.