gRPC: The Bad Parts
gRPC, a powerful RPC framework, faces challenges like a steep learning curve, compatibility issues, and lack of standardized JSON mapping. Despite drawbacks, improvements like HTTP/3 support and new tools aim to enhance user experience and address shortcomings for future success.
Read original articlegRPC, a high-performance RPC framework, has revolutionized API deployment but comes with drawbacks. Its complex learning curve, especially with unary RPCs and mandatory code generation, poses challenges. Compatibility issues with HTTP/2 and late adoption of HTTP/3 limit its reach. The absence of a standardized JSON mapping early on hinders accessibility for developers. Handling large messages in gRPC lacks a standardized approach, leading to inconsistent implementations. The community's perceived stagnation and challenging tooling also impact adoption. Despite these issues, gRPC implementations are improving, with support for HTTP/3 and tools like ConnectRPC and Buf CLI enhancing user experience. Acknowledging and addressing these shortcomings can enhance gRPC's usability and accessibility for developers, ensuring its continued evolution and success in the future.
And a lot of these features arguably have a poor cost/benefit tradeoff for anyone who isn't trying to solve Google problems. Or they introduce painful constraints such as not being consumable from client-side code in the browser.
I keep wishing for an alternative project that only specifies a simpler, more compatible, easier-to-grok subset of gRPC's feature set. There's almost zero overlap between the features that I love about gRPC, and the features that make it difficult to advocate for adopting it at work.
It solves ALL the problems of vanilla gRPC, and it even is compatible with the gRPC clients! It grew out of Twirp protocol, which I liked so much I made a C++ implementation: https://github.com/Cyberax/twirp-cpp
But ConnectRPC guys went further, and they built a complete infrastructure for RPC. Including a package manager (buf.build), integration with observability ( https://connectrpc.com/docs/go/observability/ ).
And most importantly, they also provide a library to do rich validation (mandatory fields, field limits, formats, etc): https://buf.build/bufbuild/protovalidate
Oh, and for the unlimited message problem, you really need to use streaming. gRPC supports it, as does ConnectRPC.
This made it extremely hard to get answers to questions that were undocumented.
It's a shame because I know Google can put out easy to read code (see: the go standard library).
Google claims gRPC with protobuf yields a 10-11x performance improvement over HTTP. I am skeptical of those numbers because really it comes down to the frequency of data parsing into and out of the protobuf format.
At any rate just use JSON with WebSockets. Its stupid simple and still 7-8x faster than HTTP with far less administrative overhead than either HTTP or gRPC.
- The implementation quality and practices vary a lot. The python library lacks features that the go library has because they are philosophically opposed to them. Protobuf/grpc version pinning between my dependencies has broken repeatedly for me.
- If you are a services team, your consumers inherit a lot of difficult dependencies. Any normal json api does not do this, with openapi the team can use codegen or not.
- The people who have been most hype to me in person about grpc repeat things like "It's just C structs on the wire" which is completely fucking wrong, or that protobuf is smaller than json which is a more situational benefit. My point being their "opinion" is uninformed and band-wagoning.
This article gave me some new options for dunking on grpc if it's recommended.
Not even basic syntax highlighting for IDL files in Visual Studio, but nice goodies for doing gRPC are available in Visual Studio.
My preferred approach would be to map my client to a "topic" and then any number of servers can subscribe to the topic. Completely decoupled, scaling up is much easier.
My second biggest issue is proto file versioning.
I'm using NATS for cross-service comms and its great. just wish it had a low-level serialization mechanism for more efficient transfer like grpc.
You know what's ironic, Google AppEngine doesn't support HTTP/2. Actually a lot of platforms don't.
I don't want to sound flippant, but if you don't want to learn new things, don't use new tools :D
I don't know why or how there isn't a one-liner option there, because my experience with using gRPC in C# has been vastly better:
dotnet add package Grpc.Tools // note below
<Protobuf Include="my_contracs.proto" />
and you have the client and server boilerplate (client - give it url and it's ready for use, server - inherit from base class and implement call handlers as appropriate) - it is all handled behind the scenes by protoc integration that plugs into msbuild, and the end user rarely has to deal with its internals directly unless someone abused definitions in .proto to work as a weird DSL for end to end testing environment and got carried away with namespacing too much (which makes protoc plugins die for most languages so it's not that common of occurrence). The package readme is easy to follow too: https://github.com/grpc/grpc/blob/master/src/csharp/BUILD-IN...Note: usually you need Grpc.Client and Google.Protobuf too but that's two `dotnet add package`s away.
For example if I want to import some common set of structs into my protos, there isn’t a standardized or wide spread way to do this. Historically I have had to resort to either copying the structs over or importing multiple protoc generated modules in my code (not in my protos).
If there was a ‘go get’ or ‘pip install’ equivalent for protos, that would be immensely useful; for me and my colleagues at least.
One project I worked on was basically just a system for sharing a JSON document to multiple other systems. This was at a golang shop on AWS. We could have used an S3 bucket. But sure, an API might be nice so you can add a custom auth layer or add server side filters and queries down the road. So we built a REST API in a couple of weeks.
But then the tech lead felt bad that we hadn’t used gRPC like the cool kids on other teams. What if we needed a Python client so we could build a Ansible plugin to call the API?? (I mean, Ansible plugins can be in any language; it’s a rest API, Ansible already supports calling that (or you could just use curl); or you could write the necessary Python to call the REST API in like three lines of code.) so we spent months converting to gRPC, except we needed to use the Connect library because it’s cooler, except it turns out it doesn’t support GET calls, and no one else at the company was using it.
By the time we built the entire service, we had spent months, it was impossible to troubleshoot, just calling the API for testing required all sorts of harnesses and mocks, no good CLI tooling, and we were generating a huge Python library to support the Ansible use case, but it turned out that wasn’t going to work for other reasons.
Eventually everyone on that team left the company or moved to other projects. I don’t think anything came of it all but we probably cost the company a million dollars. Go gRPC!
[0] https://fivetran.com/docs/partner-built-program [1] https://fivetran.com/docs/connectors/connector-sdk
Protobuf was designed first and foremost for C++. This makes sense. All of Google's core services are in C++. Yes there's Java (and now Go and to some extent Python). I know. But protobuf was and is a C++-first framework. It's why you have features like arena allocation [1].
Internally there was protobuf v1. I don't know a lot about this because it was mostly gone by the time I started at Google. protobuf v2 was (and, I imagine, still is) the dominant form of.
Now, this isn't to be confused with the API version, which is a completely different thing. You would specify this in BUILD files and it was a complete nightmare because it largely wasn't interchangeable. The big difference is with java_api_version = 1 or 2. Java API v1 was built like the java.util.Date class. Mutable objects with setters and getters. v2 changed this to the builder pattern.
At the time (this may have changed) you couldn't build the artifacts for both API versions and you'd often want to reuse key protobuf definitions that other people owned so you ended up having to use v1 API because some deep protobuf hadn't been migrated (and probably never would be). It got worse because sometimes you'd have one dependency on v1 and another on v2 so you ended up just using bytes fields because that's all you could do. This part was a total mess.
What you know as gRPC was really protobuf v3 and it was designed largely for Cloud (IIRC). It's been some years so again, this may have changed, but there was never any intent to migrate protobuf v2 to v3. There was no clear path to do that. So any protobuf v3 usage in Google was really just for external use.
I explain this because gRPC fails the dogfood test. It's lacking things because Google internally doesn't use it.
So why was this done? I don't know the specifics but I believe it came down to licensing. While protobuf v2 was open sourced the RPC component (internally called "Stubby") never was. I believe it was a licensing issue with some dependency but it's been awhile and honestly I never looked into the specifics. I just remember hearing that it couldn't be done.
So when you read about things like poor JSON support (per this article), it starts to make sense. Google doesn't internally use JSON as a transport format. Protobuf is, first and foremost, a wire format for C++-cetnric APIs (in Stubby). Yes, it was used in storage too (eg Bigtable).
Protobuf in Javascriipt was a particularly horrendous Frankenstein. Obviously Javascript doesn't support binary formats like protobuf. You have to use JSON. And the JSON bridges to protobuf were all uniquely awful for different reasons. My "favorite" was pblite, which used a JSON array indexed by the protobuf tag number. With large protobufs with a lot of optional fields you ended up with messages like:
[null,null,null,null,...(700 more nulls)...,null,{/*my message*/}]
GWT (for Java) couldn't compile Java API protobufs for various reasons so had to use a variant as well. It was just a mess. All for "consistency" of using the same message format everywhere.The primary misfeature of gRPC itself, irrespective of protobuf, is relying on trailers for status code, which hindered its adoption in the context of web browser without an edge proxy that could translate gRPC and gRPC-web wire formats. That alone IMO hindered the universal applicability and adoption quite a bit.
Who are you working with lol? Nobody I’ve worked with has struggled with this concept, and I’ve worked with a range of devs, including very junior and non-native-English speakers.
> Also, it doesn’t pass my “send a friend a cURL example” test for any web API.
Well yeah. It’s not really intended for that use-case?
> The reliance on HTTP/2 initially limited gRPC’s reach, as not all platforms and browsers fully supported it
Again, not the intended use-case. Where does this web-browsers-are-the-be-all-and-of-tech attitude come from? Not everything needs to be based around browser support. I do agree on http/3 support lacking though.
> lack of a standardized JSON mapping
Because JSON has an extremely anaemic set of types that either fail to encode the same semantics, or require all sorts of extra verbosity to encode. I have the opposite experience with protobuf: I know the schema, so I know what I expect to get valid data, I don’t need to rely on “look at the json to see if I got the field capitalisation right”.
> It has made gRPC less accessible for developers accustomed to JSON-based APIs
Because god forbid they ever had to learn anything new right? Nope, better for the rest of us to just constantly bend over backwards to support the darlings who “only know json” and apparently can’t learn anything else, ever.
> Only google would think not solving dependency management is the solution to dependency management
Extremely good point. Will definitely be looking at Buf the next time I touch GRPC things.
GRPC is a lower-overhead, binary rpc for server-to-server or client-server use cases that want better performance and faster integration that a shared schema/IDL permits. Being able to drop in some proto files and automatically have a package with the methods available and not having to spend time wiring up url’s and writing types and parsing logic is amazing. Sorry it’s not a good fit for serving your webpage, criticising it for not being good at web stuff is like blaming a tank for not winning street races.
GRPC isn’t without its issues and shortcomings- I’d like to see better enums and a stronger type system, and defs http/3 or raw quic transport.
Lolwut. This is what was always said about ASN.1 and the reason that this wheel has to be reinvented periodically.