Twirp RPC Explained: How GitHub Uses Twirp and How We Made Monk CI Twirp Compatible

Twirp is a lightweight RPC framework built on HTTP and Protocol Buffers. It provides typed service definitions, Protobuf serialization, predictable HTTP endpoints, generated clients and servers, and a standardized error model without introducing a custom transport layer. That makes Twirp especially useful for teams that want the benefits of RPC while keeping the network layer easy to inspect and operate.

Twirp RPC Explained: How GitHub Uses Twirp and How We Made Monk CI Twirp Compatible

GitHub maintains twirp-rs, a Rust implementation of the Twirp protocol. At Monk CI, we also had to make part of our infrastructure Twirp compatible.

The interesting part was not adding another HTTP client.

It was understanding exactly what makes a service Twirp compatible at the protocol level.

What is Twirp?

Twirp is an RPC protocol originally developed by Twitch.

Like gRPC, Twirp uses Protocol Buffers to define services and message schemas.

A service might look like this:

syntax = "proto3";

package ci.runner.v1;

service RunnerService {
  rpc AllocateRunner(AllocateRunnerRequest)
      returns (AllocateRunnerResponse);
}

message AllocateRunnerRequest {
  string repository_id = 1;
}

message AllocateRunnerResponse {
  string runner_id = 1;
}

From this definition, Twirp can generate client and server code.

But the wire protocol itself remains simple.

An RPC call becomes a normal HTTP POST request:

POST /twirp/ci.runner.v1.RunnerService/AllocateRunner

The request body is encoded using either:

application/protobuf

or:

application/json

This simplicity is one of Twirp's biggest architectural advantages.

How Twirp RPC works

A Twirp request follows a predictable flow:

.proto service definition

        ↓

generated client

        ↓

HTTP POST

        ↓

/twirp/package.Service/Method

        ↓

Protobuf or JSON payload

        ↓

server handler

        ↓

typed response

Unlike RPC systems that introduce their own transport requirements, Twirp runs over standard HTTP infrastructure.

That makes it easier to debug with existing tools, proxies, logs, load balancers, and HTTP middleware.

Twirp vs gRPC

Twirp and gRPC solve similar problems, but their transport models are different.

gRPC commonly relies on HTTP/2 and its own framing conventions.

Twirp deliberately keeps the protocol much closer to traditional HTTP.

A Twirp request is essentially:

HTTP POST
+
deterministic RPC path
+
Protobuf payload
+
standardized error format

This gives Twirp a smaller protocol surface.

For infrastructure teams, that can be useful because RPC traffic remains understandable without specialized tooling.

The tradeoff is that Twirp does not try to provide every feature available in more complex RPC systems.

Its strength is simplicity.

How GitHub uses Twirp

GitHub maintains github/twirp-rs, an implementation of Twirp for Rust.

The original Twirp implementation is written in Go, but the protocol itself is language independent.

GitHub's Rust implementation demonstrates an important property of Twirp.

You do not need the official Twirp runtime to communicate with a Twirp service.

You need to implement the protocol correctly.

In twirp-rs, Protocol Buffer definitions generate Rust clients, server interfaces, routing logic, and strongly typed request and response structures.

The implementation can therefore use Rust internally while remaining compatible with services implemented in other languages.

Conceptually:

Same .proto definition

Go Twirp service
        ↕

Twirp wire protocol
        ↕

Rust twirp-rs client

The runtime changes.

The protocol contract does not.

What Twirp compatibility actually means

This became particularly relevant for us at Monk CI.

We needed to communicate with a system expecting Twirp compatible requests.

At first glance, this looks like a normal HTTP integration.

It is not.

Sending the correct JSON or Protobuf object to approximately the right endpoint is not enough.

Twirp compatibility depends on several exact protocol details.

The client and server must agree on:

  1. 1The Protobuf package
  2. 2The service name
  3. 3The RPC method name
  4. 4The generated HTTP path
  5. 5The request message schema
  6. 6The response message schema
  7. 7The serialization format
  8. 8The content type
  9. 9Twirp error semantics

For example, this RPC:

rpc AllocateRunner(AllocateRunnerRequest)
    returns (AllocateRunnerResponse);

might map to:

/twirp/ci.runner.v1.RunnerService/AllocateRunner

That path is part of the contract.

If the package or service name differs, the request reaches a different endpoint even if the payload itself is completely valid.

Why Protobuf field numbers matter

One subtle source of incompatibility is the Protobuf schema itself.

Consider:

message AllocateRunnerRequest {
  string repository_id = 1;
}

The number 1 is not cosmetic.

It is the actual field identifier used in the binary Protobuf representation.

Changing the field name does not necessarily break the wire format.

Changing the field number can.

This means Twirp compatibility is closely tied to maintaining compatible Protobuf schemas.

That matters when services evolve independently.

Once a field number has been used, it should not casually be reassigned to another field.

Twirp error handling

Successful RPC calls are only half of the protocol.

Errors also need to be compatible.

A Twirp error response is represented using a standardized JSON structure such as:

{
  "code": "invalid_argument",
  "msg": "repository_id is required"
}

Twirp defines known error codes including:

invalid_argument
not_found
permission_denied
unauthenticated
unavailable
internal

These codes provide semantic meaning beyond raw HTTP status codes.

A Twirp client therefore needs to understand both successful Protobuf responses and structured Twirp errors.

This was important for our Monk CI integration.

If the happy path works but error parsing is incorrect, the integration is still not fully Twirp compatible.

Why Twirp supports both Protobuf and JSON

Twirp supports both Protobuf and JSON payloads.

Protobuf is generally the better option for service to service communication because it provides compact binary serialization and strongly defined schemas.

JSON is extremely useful for debugging.

For example, engineers can inspect or reproduce a Twirp request using standard HTTP tools.

This gives Twirp an interesting operational property.

Production traffic can remain typed and efficient.

Debugging can remain human readable.

Making Monk CI Twirp compatible

For Monk CI, the implementation work was mostly about treating Twirp as a wire protocol instead of a framework dependency.

The flow effectively became:

Monk CI

   ↓

construct generated Protobuf request

   ↓

serialize request

   ↓

POST to exact Twirp RPC path

   ↓

receive Protobuf response
or Twirp error

   ↓

decode into internal application model

This separation was important.

Our internal architecture did not need to become Twirp specific.

Only the integration boundary needed to understand Twirp.

That meant our business logic could remain independent from the RPC protocol.

Twirp as an interoperability layer

GitHub's twirp-rs implementation illustrates the same principle at a larger level.

One service might be written in Go.

Another component might be written in Rust.

A third could use a custom Twirp implementation.

As long as all three agree on:

.proto schema
RPC route
serialization
content type
error model

they can communicate.

This is one of the most useful architectural properties of Twirp.

Compatibility is defined by a relatively small wire contract rather than a large shared runtime.

When Twirp makes sense

Twirp is particularly useful when you want strongly typed RPC APIs without making the networking layer unnecessarily complex.

It works well for internal service communication where teams want:

Protocol Buffers
generated clients
strong API contracts
standard HTTP infrastructure
simple debugging
language independent services

It may also be attractive when HTTP observability and operational simplicity matter more than advanced RPC transport features.

The architectural lesson from Twirp

The most interesting thing about Twirp is not that it replaces HTTP.

It keeps HTTP visible.

Protocol Buffers define the data.

The service definition defines the operations.

Twirp defines routing and error semantics.

Generated code handles repetitive serialization and request plumbing.

The application remains responsible for the actual business logic.

That separation is what made Twirp compatibility useful for Monk CI.

We did not have to redesign our system around an RPC framework.

We only had to implement the boundary correctly.

And GitHub's Rust implementation demonstrates the same underlying idea.

Different languages.

Different runtimes.

Same protocol.

That is the real value of Twirp.

Aishwarya Palta

Last updated September 19, 2026