gRPC: Top 6 Things that Bite Newbies

Charles Thayer
7 min readMar 13, 2021

At work, we’ve been answering a lot of questions lately from people moving off REST to gRPC, and there are a number of stumbling blocks and hurdles that I’d like to call out for those moving to gRPC for the first time.

But just in case you’re unsure about gRPC, first here’s some motivation, along with a couple of caveats.

Why it’s Awesome

Before diving into the tips, here’s what I’ve found super valuable about gRPC and IDLs, interface definition languages, in general:

Clear Contract w/ Customers: Having an IDL makes it super clear and unambiguous what’s expected in your APIs, even if your service is just for an internal customer. Less back and forth and clarifications needed about the meaning of arbitrary strings. Because it’s strongly typed with a well-defined schema, it’s easy to publish protobufs widely, knowing that people can use them in all types of programming languages and environments. Google has published around 48K proto files publicly at the time of writing.

Write once, get clients/servers: Anyone who’s had to support APIs and clients for a variety of different languages (polyglot) will tell you it’s a boring and tedious pain in the butt. With gRPC, you get a ton of languages for free, and writing a mock server is trivial since you can just generate another stub server from the proto files. (C++, Java, Go, C#, node/javascript, Python, Objective-C, etc.)

Doesn’t break customers: It’s “made to migrate”. gRPC has very strong support for evolving APIs, and not breaking backward compatibility. When I worked on a startup shipping a mobile app, I realized how many old client versions can be floating around as you push people to install your latest app. Not fun, plus forcing updates causes serious customer churn.

High-performance oriented: The data is binary encoded to around half the size of REST/Json, it’s easier to encode and decode so it typically uses half the CPU and is faster, and last but not least it’s HTTP/2 which maintains streams and multiplexes requests so TCP can work far more efficiently.

Etc: Then are a few more features like security (SSL), inline documentation, extensibility through protobuf annotations, and probably many others by the time you’re reading this.

Mobile: There’s a lot of excitement in the mobile world about moving to gRPC because many of these features are particularly important for mobile APIs. For example, less data is cheaper for mobile data connections, but also, when on the open Internet, it greatly reduces the probability of lost and dropped packets. There’s also the backward compatibility to help manage clients running a wide variety of Apps versions (or client libs).

Cons: Having said this, it does bring some complexity. It’s a binary protocol so it’s less human inspectable and instead of using curl and postman, you’ll need to debug with bloomrpc or insomnia. Within a datacenter, for private services without PII, it may be overkill to have SSL getting in the way of debugging.

Top 6 Tips

#0. Philosophy

First some critical concepts for the back of your mind. All the tips are fundamentally related to a few key ideas:

  1. Always Backward Compatible: We’re used to making liberal changes to our code, but once you have customers depending on the APIs being stable, you can’t just update parameters without breaking clients.
  2. WORA (write once, run anywhere): Because it’s polyglot (works in many languages) there may be parts of the proto files (protobufs) that feels a little unnatural for your platform. You need to keep in mind that a client can be anything from python to C++ so the naming and name-spacing rules may not match what you’re used to.
  3. REST vs RPC: Nouns vs Verbs. Lastly, coming from REST you may be thinking in terms of objects and CRUD operations. Going to gRPC is a bit more function oriented (Operation-oriented in gRPC lingua), although it’s still very natural for object-oriented languages. Which brings us to the tips…

But just to clarify the terminology, Protocol Buffers (protobufs) are the serialization/deserialization mechanism and the IDL (interface definition language), which is written in proto files (the *.proto files), and gRPC is the network protocol that exchanges messages encoded in protobufs.

#1. OOPs Don’t Worry

Have no fear, protobufs translate easily into your modern object-oriented programming languages, though the terminology is slightly different:

  • class => “service”. A set of methods acting on structured data.
  • Functions (and methods) => “rpc
  • struct => “message
  • Arrays [] => “repeated” (e.g. int_32 a[] == repeated int32 a)
  • include (using) => “import
  • union => “oneof

So, let’s say you have some code like this, depending on your language:

struct HelloRequest {
string name;
};
struct HelloReply {
string message;
};
class Greeter {
public static HelloReply sayHello(HelloRequest request);
};

The proto file (for all target languages) would be:

syntax = "proto3";
// ...
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}

#2. Take and Return Messages

For something like an echo server you might be tempted to take and return a string:

syntax = "proto3";service EchoService {
rpc Echo (string) returns (string);
}

But don’t! Instead, you always want to take and return a message type (struct) so that your inputs and outputs can evolve without breaking backward compatibility:

syntax = "proto3";service EchoService {
rpc Echo (EchoRequest) returns (EchoResult);
}
message EchoRequest {
string msg = 1;
}
message EchoResult {
string msg = 1;
}

Now if you find you need a status code in the result, you can safety change the EchoResult without affecting EchoRequest

message EchoResult {
string msg = 1;
int32 status = 2;
}

#3. Same Shape, Different message Please.

Even though two messages may look very similar, and have the same fields and types, resist the urge to use a single message type if they have separate meaning. For example, in the EchoService above, one might have naturally chosen to have EchoRequest and EchoResult be one message instead since they each just have a string field named msg; however, as shown above, having separate message types allowed us to update the EchoResult without affecting the EchoRequest. Again, backward compatibility for-the-win!

#4. Missing or New fields show up with a Default Value

In the old days (proto v2), there were annotations for required and optional fields, but now they’re all just optional. When a field is missing it arrives with the default value of empty-string “”, 0, or false. This let’s you make smart decisions to maintain backward compatibility. In the above example where we added a new status field to EchoResult, an old client who didn't know about it would send the old format without status, so it would arrive as 0.

So this has another interesting consequence for enum types. These too will come in with the value 0 from older clients who don’t know about them, so to indicate that a field is missing, you’ll always make the first enum value be something like “NONE” or “UNDEFINED”, for example:

message EchoResult {
string msg = 1;
enum StatusCode {
NONE = 0; // default if missing
SUCCESS = 1;
BAD_INPUT = 2;
}
StatusCode status = 2;
}

#5. How to Rename or Remove fields

Good and bad news: although renaming is actually fine for fields because the binary serialization doesn’t include the field name (just those “= 2;” style numbers), you almost never want to do this because you’ll break the generated client and server code stubs. It’s more likely that you’ll want to mark the old one deprecated, and add the new one:

message EchoResult {
string msg = 1;
int32 status = 2 [deprecated = true]; // <- Changed
enum StatusCode {
NONE = 0;
SUCCESS = 1;
BAD_INPUT = 2;
}
StatusCode status_code = 3; // <- New name and field #3
}

Here, older clients may continue sending the “status” field #2 but the new status_code will be the default value, NONE, so your code should handle that. New clients will send the new status_code with a value (not NONE) and if you read the old status field #2, it would be 0 but your compiler will probably give you a warning (language dependent). Note that the old field #2 isn’t going out on the wire protocol, so it’s not costing you anything.

#6. Remember Polyglot

Frustration. There are going to be times when the naming in gRPC feels insane. The length and namespacing of variables is going to get super long. The CamelCase or snake_case is going to trip you up a dozen times. You’re going to expect dots ‘.’ where there are underscores “_” and vice versa.

This pain is all in the name of supporting dozens of different languages, from Lua to C++, where the rules vary wildly. Just know it’s going to be a pain and be ready to carefully check your spellings first when there are strange errors like “Operation not found”.

Other Tips

Okay you over-achievers, I didn’t want to let this article get too long, but this is a catch all for additional things that can be added over time, to highlight for newbies to gRPC

  • Beware proto2! Many online examples are still proto2, but the Internet goes stale fast. Be sure to learn from proto3 tutorials.
  • Well Known Types: avoid the urge to define a common data types for your set of protos, if you can use one of the “WKT” (well known types) here that includes things like Duration and Timestamp.
  • API vs Storage messages: It’s fine to use protobufs to define APIs and physical storage, but avoid the urge to use a single message. Instead define two separate messages that can evolve on their own.

Next Steps: here are some links for beginners taking the next steps in learning how to build with gRPC:

Conclusion

Hopefully you feel confident enough to get started and clear these little hurdles. This is just my experience and perspective, but I’d love to hear yours. What tips do you have? Please let me know in the comments.

--

--

Charles Thayer

Roblox, Facebook, Yahoo YST, Distributed Systems, KV & NoSQL, Monitoring, DevOps, CTO, Principal Engineer, etc.