Why not gRPC?
This code generation approach is not a novel idea at all. Google provides a framework, gRPC, which does a very similar thing, and gRPC has grown to be pretty prominent. In fact, we started out at Twitch using gRPC. It didn’t gain a lot of traction, though — we had some problems with it, and these problems spurred us to create Twirp. There were four core problems in gRPC for us which Twirp solved:
1. Lack of HTTP 1.1 support: gRPC only supports http/2, because its protocol relies upon HTTP Trailers and full-duplex streams. Twirp supports HTTP 1.1 and http/2. This is important because many load balancers (both hardware and software) only support HTTP 1.1 — including AWS’s ELBs, which sit in front of EC2 instances. The downside is that Twirp doesn’t support streaming RPCs, but those are rare at Twitch — we’ve found that pretty much all of our services have request-response workloads.
2. Large runtime with breaking changes: gRPC is very complex, so the Go generated code is relatively thin and calls into a large runtime called grpc-go. The generated code and the runtime library are tightly linked and need to match closely. Unfortunately, that runtime has seen breaking changes, sometimes without warning or explanation. This would be merely annoying, but becomes a real pain when you have a large network of services communicating amongst themselves, importing each others clients.
Breaking changes in the runtime mean that old client code no longer compiles; this means that clients need to use the same gRPC runtime version as that of the services they depend on. The same is true for those services dependencies, and quickly this means that all of us at Twitch need to use the same version of gRPC in lockstep. Go dependency management is famously rough, so in practice this means we could never upgrade without everyone stopping work and coordinating an upgrade together, even in the face of bugs.
To make matters worse, the grpc-go runtime requires a particular version of the Go protobuf library, github.com/golang/protobuf, also enforced at the compilation level — so we had the same problem ever upgrading protobuf. In practice, we almost never really upgraded, even in the face of severe bugs.
In contrast, Twirp sticks almost everything into the generated code, so it’s fine for different services to upgrade at their own pace. We’ve taken compatibility-breaking changes extremely seriously and helped legacy systems continue to work.
3. Bugs due to the complex runtime: grpc-go includes a complete http/2 implementation, independent of the standard library, and introduces its own flow-control mechanisms. This stuff is very difficult to understand quickly and can lead to confusing, counterintuitive, and even totally-broken behavior (that last bug caused several outages at Twitch). Leaks are not unheard of because of the internal complexity of the project, which is a complete deal-breaker for long-running, high-availability services.
Twirp, by contrast, can use plain old HTTP 1.1, which might not be blazingly efficient but at least it’s simple and we know how to work with it, and the standard library’s HTTP 1.1 implementation is rock-solid. And if you do need the extra boosts of http/2, Twirp can use it too — it doesn’t ditch the efficiency, just the complexity.
This isn’t meant as a bash on the grpc-go team. They do terrific work. But the reality is that the standard library’s HTTP implementation is always going to be better-tested and more broadly used than the custom one in grpc-go.
4. Difficulty working with binary protobuf: gRPC only supports binary protobuf payloads. Twirp supports the binary encoding, but also supports protobuf’s JSON encoding for payloads, using the official JSON mapping spec.
Allowing JSON has two advantages: for one, makes it easier to write cross-language and third-party clients of Twirp servers — getting the protobuf right by hand is really hard, but getting JSON right by hand is doable.
Second, it makes it easier to write quick command-line cURL requests to debug a running server on the fly. This is the sort of small quality-of-life thing that really can make a difference in the long run, and can especially help when first setting a service up — gRPC felt completely opaque, while it’s clear how to quickly check your new Twirp service.
That said, gRPC does have some benefits. It might be worth the costs for you. In particular, gRPC supports bidirectional streaming RPCs, sending flows of uninterrupted data back and forth between client and service. Twirp has nothing like this — just plain old request-and-response. We haven’t really missed this at Twitch, but it might be important for your problems, and if so, gRPC is pretty much the only game in town.
And Twirp takes a minimalist approach (the server is just a http.Handler, the client is nothing special), grpc-go is practically overflowing with ambitious add-ons and extras, from a load balancing library to a name resolution framework to let you plug in your own DNS alternative, if that’s your thing. We’ve preferred Twirp’s modular approach, letting dedicated load balancing software handle load balancing, but grpc-go’s all-in-one system might interest you.