Apache Thrift
Thrift came out of Facebook in 2007 and was donated to the Apache Foundation a year later. It does a similar job to gRPC + Protobuf — describe a service in an IDL, generate typed client and server code, talk in a binary format. The interesting design choice is that the transport layer and the wire-format layer are pluggable, so the same generated code can speak HTTP, raw TCP, or in-process pipes, with binary, compact, or JSON encoding. That flexibility is the reason Thrift survived inside companies that built their own RPC stacks on top of it.
What Thrift actually is
Thrift is two things bundled into one project. The first is an interface definition
language: a small, typed schema language you write in .thrift files to
describe data structures and the services that pass them around. The second is a code
generator plus a set of runtime libraries that turn that schema into client and server
code in a particular language. Run the thrift compiler over a schema and you
get typed classes for every struct, a typed client you can call methods on, and a server
skeleton you fill in with your own logic. The two sides agree on the wire format because
they were generated from the same file.
That much sounds exactly like gRPC with Protobuf, and the resemblance is not an accident. Thrift came first. Facebook built it internally in 2006 and 2007 to connect services written in different languages without hand-writing serialisation code for each pair, and released the original tech note in 2007. gRPC arrived in 2015, well after Thrift was in heavy production use across several large companies. So the way to read the comparison that runs through this page is not "Thrift copied gRPC" but the reverse: gRPC is a later, more opinionated take on a problem Thrift solved first, and the two have been converging on the same ideas ever since.
The defining difference is right there in how the projects draw their boundaries. gRPC fixes the transport (HTTP/2) and the serialisation (Protobuf) and treats them as part of the contract. Thrift refuses to fix either. It splits the runtime into separate layers — how bytes are encoded, how bytes are moved, how requests are dispatched — and lets you swap each one independently. The rest of this page works through that split, the IDL that feeds it, how schemas change over time without breaking callers, and when any of it is the right tool today.
An IDL example
Thrift schemas live in .thrift files. The syntax is similar to Protobuf
but with a few extra concepts — exceptions, services as first-class entities, typedefs.
namespace go billing
namespace java com.example.billing
enum Status {
PENDING = 1,
SETTLED = 2,
FAILED = 3,
}
struct Charge {
1: required string id,
2: required i64 amount_cents,
3: required string currency,
4: optional Status status,
5: optional list<string> tags,
}
exception NotFound {
1: required string id,
}
service BillingService {
Charge get(1: string id) throws (1: NotFound nf),
Charge create(1: Charge new_charge),
void refund(1: string id),
}Field IDs (1, 2, 3...) play the same role as Protobuf field tags — they identify fields
on the wire. required and optional are real keywords in Thrift,
unlike proto3 where required was removed. Most modern style guides treat
everything as optional anyway because required makes evolution harder.
A few things in that file have no clean equivalent in Protobuf. exception is
a first-class declaration: a struct that the generated code knows how to raise and catch
on the calling side, so a method that fails surfaces a typed error rather than a status
code you have to interpret. service is also a real keyword, and a service can
extend another service the way a class extends a base class, which lets you layer a richer
interface on top of a shared one. The container types — list, set,
and map — carry their element types in the schema, so a
map<string, list<Charge>> generates as the natural nested
collection in each target language rather than as an untyped blob.
The base types are deliberately small and chosen to map onto every target language without
surprises. Thrift has bool, byte (an 8-bit signed integer),
i16, i32, and i64 for integers,
double for floating point, string for text or arbitrary bytes,
and binary when you specifically mean raw bytes rather than UTF-8. There is no
unsigned integer type on purpose, because several target languages do not have one and a
schema that quietly relies on unsigned wraparound would behave differently in Java than in
C++. A typedef lets you give a meaningful name to one of these, so
typedef i64 Timestamp documents intent without changing the wire format.
From one IDL to a client and a server
The code generator is the part you touch most often. You run the compiler once per
language you care about, point it at a schema, and it writes a tree of source files. Two
pieces matter for every service: a typed client and a server skeleton. The client exposes
the methods from the service block as ordinary functions in the target
language; call client.get("c_123") and the generated code serialises the
arguments, sends the request, waits for the reply, deserialises it, and either returns a
Charge or raises a NotFound. The server skeleton is the mirror
image: it defines an interface (or abstract class) with the same methods, and you write a
handler that implements the actual behaviour. Everything between the call and the handler —
framing, encoding, dispatch — is generated or supplied by the runtime library.
This is what makes Thrift attractive in a polyglot organisation. The same
billing.thrift can produce a Go client, a Java server, a Python admin script,
and a C++ batch job, and all four agree on the format because the compiler derived them
from one file. Nobody hand-writes a parser, and when the schema changes you regenerate
rather than hunt down every place a field was read by hand. The generated code is checked
in or built as part of the pipeline; either way the schema is the single document the whole
fleet trusts. Thrift's published language list runs to around twenty-five targets, though
the quality varies a lot between the popular ones and the long tail.
The layered architecture
Where Thrift differs most from gRPC is its layering. The generated code does not know how messages are encoded or how bytes travel. It calls into a protocol object, which knows how to turn a struct into bytes and back, and the protocol calls into a transport, which knows how to move those bytes from one place to another. Above the generated code sits a processor on the server that reads the method name off the wire and dispatches to the right handler, and a server that owns the accept loop and the threading model. Each of these is a separate, swappable object.
The point of cutting it this way is that the four concerns are independent. How you encode a struct has nothing to do with how you ship the bytes, which has nothing to do with how the server schedules work. So you can mix and match.
| Layer | Choices |
|---|---|
| Protocol | TBinaryProtocol, TCompactProtocol, TJSONProtocol, TSimpleJSONProtocol |
| Transport | TSocket, THttpClient, TMemoryTransport, TFileTransport, TPipe |
| Server | TSimpleServer, TThreadPoolServer, TThreadedServer, TNonblockingServer |
In practice, most production deployments pick TCompactProtocol over TSocket with a TThreadedServer or TNonblockingServer. The other combinations exist for edge cases — for example, TJSONProtocol over THttpClient turns Thrift into something that looks like a JSON-RPC service to outside callers.
The protocol layer is the one you reach for when size or readability matters. TBinaryProtocol is the simplest: every field is written as a type byte, a field ID, and a value, with integers in fixed-width big-endian form. It is easy to debug and a little wasteful. TCompactProtocol keeps the same logical structure but squeezes it with variable-length integers and a few tricks covered in the next section, and it is the default most teams settle on. TJSONProtocol writes valid JSON, which is slow and large but lets a human read a captured message or lets a non-Thrift client poke at the service. The choice is per-connection and lives entirely in the protocol object; nothing above it changes.
The transport layer answers a different question: where do the bytes go, and how are they framed. TSocket is a plain TCP connection. A buffered transport wraps another transport and batches small writes so you are not making a syscall per field. TFramedTransport prefixes each message with its length, which is what the asynchronous, non-blocking servers need because they have to know where one message ends before they can hand it off. THttpClient tunnels Thrift inside HTTP requests, useful when a firewall or proxy only speaks HTTP. A memory transport reads and writes a byte buffer, which is how you serialise a struct to a blob for storage or a test without touching the network at all. Because a transport can wrap another transport, framing and buffering stack cleanly on top of the raw socket.
The server layer is purely about concurrency. TSimpleServer handles one connection at a time and exists mainly for tests. TThreadPoolServer dedicates a thread per connection from a bounded pool, which is fine until connection count outgrows thread count. TNonblockingServer uses an event loop and a small worker pool to handle many connections without a thread each, at the cost of requiring a framed transport. Picking a server is a capacity-planning decision, and it has no effect on the wire format, so a thread-pool server and a non-blocking server can serve the very same clients.
The Compact Protocol on the wire
TCompactProtocol is the encoding most teams use today. The format is similar to Protobuf — variable-length integers, ZigZag for signed values, field IDs as tags. The notable differences:
- A field's delta from the previous field's ID is encoded, not the absolute field ID. So sequences of consecutive field IDs cost one nibble each.
- Booleans are packed into the field-header byte itself rather than as a separate value byte. A struct of all-boolean flags is unusually compact.
- A trailing zero byte marks the end of a struct, instead of a length prefix. Streaming parsers can decode without knowing the size up front; size-prefixed parsers can't.
On typical structs the wire size is within a few percent of Protobuf, sometimes smaller and sometimes larger depending on the shape of the data.
It helps to see why the delta encoding pays off. A struct's fields are written in ID order, so a struct with fields 1, 2, 3, 4 produces deltas of 1, 1, 1, 1 — each a single nibble rather than a full field ID. Only when you skip ahead, say from field 4 to field 17, does the encoder fall back to writing the absolute ID. This is why the advice to number related fields consecutively is not just tidiness; it keeps the field headers down to one byte each. ZigZag handles the other common waste. A naive variable-length integer spends many bytes on small negative numbers because their two's-complement form has high bits set; ZigZag maps −1, 1, −2, 2 to 1, 2, 3, 4 so that small magnitudes stay small regardless of sign.
A subtle consequence of the trailing-zero struct terminator is that a TCompactProtocol parser can read a struct as a stream, stopping when it hits the stop byte, without being told the struct's length in advance. Protobuf, by contrast, length-prefixes embedded messages, so a Protobuf parser always knows the size of a nested message before it reads the body. Neither approach is strictly better; the streaming-friendly terminator suits Thrift's pluggable transports, where a transport may not know the total size, while the length prefix lets a parser skip an unknown field cheaply by jumping over it.
What an RPC call looks like on the wire
A Thrift method call is not just the serialised arguments. The protocol wraps them in a
message envelope: the method name, a message type (call, reply, exception, or one-way),
and a sequence ID the client uses to match a reply to the request it sent. The arguments
themselves are encoded as if they were the fields of an anonymous struct — argument 1 is
field 1, argument 2 is field 2, and so on — which is why arguments carry IDs in the IDL
(get(1: string id)) and why the same evolution rules that apply to structs
also apply to argument lists. The reply is encoded the same way: a struct whose field 0 is
the return value and whose other fields are the declared exceptions.
Two details fall out of this envelope. A one-way method — declared with the
oneway keyword — uses a message type that tells the server not to send a reply,
so the client returns as soon as the bytes are flushed. And because plain Thrift sends one
call at a time over a connection and waits for its reply, a client that wants many
in-flight calls on one connection needs a multiplexing layer above the base protocol. That
gap is exactly what the Finagle and Mux runtimes filled, and it is one reason a heavy Thrift
user usually runs a fork rather than stock Apache.
Schema evolution rules
The rules look familiar if you've worked with Protobuf:
- Adding a new optional field is safe.
- Removing a field is safe if no client requires it.
- Never reuse a field ID. There is no
reservedkeyword in stock Thrift, so the discipline has to live in code review. - Type changes are mostly unsafe; the small number of compatible changes are documented in the Thrift IDL spec but rarely used.
requiredis a one-way door. Going from optional to required is a breaking change for old serialisers; going from required to optional is a breaking change for old parsers. The pragmatic advice is "never userequiredfor anything you might want to remove".
The mechanism behind these rules is the same one Protobuf uses. Because every field is tagged with its ID on the wire, a parser that meets a field ID it does not recognise can read its type, skip the right number of bytes, and carry on. So a new server that adds field 6 can still read messages from an old client that stops at field 5, and an old client can read a message from the new server by ignoring field 6 entirely. The ID, not the position or the name, is the contract. Rename a field and nothing breaks on the wire, because the name never travels; change a field's ID and everything breaks, because every reader is now looking for it in the wrong place.
This is also why versioning in Thrift is mostly a discipline rather than a feature. There is
no version number in the schema and no built-in notion of "v2 of this struct." You evolve a
type in place by adding new optional fields and leaving the old ones alone, and both sides
of a rolling deploy keep working because each ignores what it does not understand. The one
place this falls down is required, which removes the parser's freedom to skip:
a required field that is missing makes deserialisation fail outright. That single keyword is
the difference between a forgiving format and a brittle one, which is why proto3 dropped its
equivalent and why most Thrift style guides treat required as something close
to forbidden.
Stock Apache Thrift has no reserved keyword to fence off a retired field ID, so
the rule "never reuse an ID" has to be enforced by people. The usual practice is to leave a
comment where a field was removed and let code review catch a careless reuse. Teams that
care about this at scale wrap the compiler in a CI check that diffs the schema against its
previous version and rejects any commit that re-tags an existing ID, since there is no
buf lint equivalent in the box. The format will happily let you reuse an ID; it
just produces silent data corruption when an old message is read under the new meaning.
fbthrift, Finagle Thrift, and Thrift Mux
The Apache Thrift implementation is the lowest common denominator. Inside the companies that depend on Thrift, two forks have grown into substantially different runtimes:
- fbthrift. Facebook's fork. Production-grade C++ and Python servers, support for streaming, Hack/PHP code generation, and bespoke optimisations for Facebook's network fabric. Now open-source, but most of the documentation lives in the source tree.
- Finagle Thrift. Twitter's RPC framework. Adds rich client-side features — load balancing, retries, circuit breaking, request hedging — over a Mux-based transport that multiplexes many calls onto a single connection. If you've used Twitter's older open-source projects (Finagle, Finatra), this is what they ran on top of.
If you're considering Thrift for a new project today, you're really considering one of these forks; Apache Thrift on its own doesn't have the operational features modern service meshes assume.
Thrift vs gRPC + Protobuf
| Property | Thrift | gRPC + Protobuf |
|---|---|---|
| IDL surface | Services, structs, exceptions, typedefs, enums | Services, messages, enums (no exceptions; errors are status codes) |
| Default wire format | TCompactProtocol | Protobuf |
| Transport | Pluggable (TCP, HTTP, pipes, in-memory) | HTTP/2 (gRPC-Web for browsers) |
| Streaming | Server-side via fbthrift; not in stock Apache | Bidirectional; built in |
| Language coverage | ~25 languages, varying quality | ~12 languages, all first-class |
| Ecosystem in 2025 | Strong inside Facebook/Pinterest/Uber, modest elsewhere | Default RPC stack across most of the industry |
For a brand-new service with no existing investment in either, gRPC is usually the easier choice — better documentation, more examples, broader operational tooling. If you're already in a Thrift environment (especially fbthrift), there's rarely a strong reason to migrate; the runtime characteristics are similar.
The cleanest way to hold the comparison is to separate the two halves of each project. The serialisation formats — TCompactProtocol and Protobuf — are close cousins. Both tag fields with numeric IDs, both use variable-length integers and ZigZag, both skip unknown fields for forward compatibility, and both land within a few percent of each other on real data. If you only care about the bytes, the choice barely matters. The RPC halves are where they diverge. gRPC standardised on HTTP/2, which gives it multiplexing, streaming, flow control, and an enormous amount of off-the-shelf infrastructure — load balancers, proxies, and service meshes already speak it. Thrift left the transport open, which was the right call in 2007 when HTTP/2 did not exist, but it means stock Thrift has no agreed answer for multiplexing or streaming and leans on forks to supply them.
The other axis worth naming is the error model. Thrift puts typed exceptions in the IDL, so a failure can be a specific, named thing the caller catches. gRPC models errors as status codes plus optional detail messages, which is simpler but coarser. Neither is obviously right; the exception model reads more naturally in languages built around exceptions, while the status-code model travels better across language boundaries that handle errors differently. If you are weighing this against the broader question of RPC versus request-response over HTTP, the gRPC vs REST comparison covers the trade-off that sits one level up from the wire format.
Where Thrift still lives
gRPC won the open-source mindshare contest, and for a service starting fresh in 2025 it is the default almost everywhere. But Thrift did not disappear; it retreated into the places where it was already load-bearing. The largest of those is Facebook, where fbthrift carries a very large share of internal service traffic and the schema language is woven through the codebase too deeply to replace. Pinterest, Uber, and a handful of other companies that adopted Thrift early run substantial fleets on it for the same reason — the cost of migration dwarfs any benefit, and the runtime characteristics are close enough that there is nothing to gain.
The second place it survives is polyglot organisations that valued Thrift's wide language coverage before gRPC matured. A shop with services in Java, C++, Python, and PHP could generate clients for all of them from one schema years before gRPC's language support was as even. Where that investment is sunk and working, there is little pressure to move. The honest summary is that Thrift today is a brownfield technology: you meet it because it is already there, you keep it because it works, and you reach for gRPC when you get to start from scratch.
Common mistakes
- Mixing protocol versions across services. A client using TBinaryProtocol cannot talk to a server using TCompactProtocol. The two formats don't have a magic byte to distinguish them.
- Treating
requiredas a documentation hint. It's enforced at deserialisation time. A field that becomesrequiredin a later schema version will reject every message produced by older clients. - Ignoring exceptions in service definitions. If a method declares
throws, the generated client will surface those exceptions. If you later add a new exception type, old clients will see it as a generic transport error instead of the specific exception. - Writing the IDL once and never linting it. There's no
buf lintequivalent for Thrift. Style and breaking-change rules have to be enforced in code review or with custom CI scripts.
The trade-offs, plainly
The flexibility that defines Thrift is also its main liability. Letting protocol and transport vary independently pays off when you are building your own RPC stack on top of it, which is what Facebook and Twitter did. But that same openness means stock Thrift ships without the things a modern service expects out of the box: no agreed multiplexing, no streaming in the base implementation, no standard story for deadlines, retries, or load balancing. gRPC fixed its transport precisely so it could bake those in. Thrift hands you a toolkit and trusts you to assemble the operational layer, which is fine if you have a platform team and a problem otherwise.
The tooling gap is the other recurring cost. There is no widely adopted linter, no breaking- change detector in the box, and the documentation for the parts people actually run — the forks — mostly lives in source trees rather than polished guides. A team adopting Thrift today inherits the job of building the guardrails that gRPC's ecosystem provides for free. Set against that, the wins are real where they apply: broad language coverage from one schema, a serialisation format as tight as Protobuf, typed exceptions that read naturally, and a layered design that lets you swap encodings and transports without rewriting the code above them. Whether those wins matter usually comes down to one question — are you already in a Thrift shop, or are you free to choose. If you are free to choose, the easier road is gRPC. If you are not, Thrift is a solid place to already be.
Further reading
- Apache Thrift — IDL reference
- Slee, Agarwal, Kwiatkowski (2007) — Thrift: Scalable Cross-Language Services Implementation — the original Facebook tech note.
- TCompactProtocol specification
- facebook/fbthrift — Facebook's actively-maintained fork.
- Twitter's Finagle — the framework Finagle Thrift is part of.