ProtoBus for Python
RabbitMQ-native microservices for Python, with Protocol Buffers on the wire.
Define a service in a .proto file, implement it as a class, and call it from
anywhere on the bus as if it were local. ProtoBus turns each service into one
durable RabbitMQ queue with N processes competing for it — so load balancing,
failover, backpressure, retries and dead-lettering are the broker's, not
Python's.
This is the Python port of protobus (TypeScript). The two are wire-compatible: a Python service serves TypeScript callers and vice versa, streaming, events, custom types and error codes included. Every commit runs a cross-language suite in both directions against a live broker and a pinned TypeScript revision (2.5.0) to keep it that way; the behavioural differences that remain are listed in Known Issues.
Install
pip install protobus
You also need a RabbitMQ 3.8+ broker:
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management-alpine
No protoc is needed: .proto files are parsed at runtime.
Quick start
Four steps to a working RPC.
1. Describe the service
// proto/Calculator.proto
syntax = "proto3";
package Calculator;
message AddRequest {
int32 a = 1;
int32 b = 2;
}
message AddResponse {
int32 result = 1;
}
service Math {
rpc add(Calculator.AddRequest) returns(Calculator.AddResponse);
}
Package plus service name is the service's name on the bus: Calculator.Math.
2. Implement it
# calculator_service.py
from protobus import RunnableService
class CalculatorService(RunnableService):
service_name = "Calculator.Math"
async def add(self, request: dict, actor: str, correlation_id: str) -> dict:
return {"result": request["a"] + request["b"]}
3. Run it
# server.py
import asyncio, os
from protobus import Context
from calculator_service import CalculatorService
async def main():
context = Context()
await context.init(os.environ.get("AMQP_URL", "amqp://localhost"), ["./proto"])
await CalculatorService.start(context) # blocks until SIGINT/SIGTERM
asyncio.run(main())
RunnableService.start installs signal handlers and, on shutdown, stops
taking new work, drains in-flight messages, runs your cleanup() hook and
disconnects — in that order.
4. Call it
# client.py
import asyncio, os
from protobus import Context, ServiceProxy
async def main():
context = Context()
await context.init(os.environ.get("AMQP_URL", "amqp://localhost"), ["./proto"])
calculator = ServiceProxy(context, "Calculator.Math")
await calculator.init()
response = await calculator.add({"a": 5, "b": 3})
print(f"5 + 3 = {response['result']}")
await context.close()
asyncio.run(main())
$ python client.py
5 + 3 = 8
Full walkthrough, including events and the project layout: Getting Started.
Why ProtoBus
RabbitMQ only, on purpose
ProtoBus is built for one broker, so the things a broker is good at stay in the broker instead of being reimplemented above it:
| Concern | Where it lives |
|---|---|
| Load balancing | competing consumers on one queue |
| Routing | topic exchange bindings (REQUEST.<Service>.*) |
| Redelivery on consumer loss | late ack — an unacked delivery returns to the queue |
| Retry delay | the retry queue's x-message-ttl, drained by DLX |
| Persistence | durable queues, persistent messages |
| Dead letters | a real <Service>.DLQ |
| Priority | native queue priorities |
A request goes publisher → exchange → queue → consumer. Nothing tracks live instances, so nothing holds a stale one, and a consumer that dies mid-request leaves its delivery unacked for the next consumer to take.
The cost of this is written down rather than glossed over — read Delivery Guarantees before you rely on any of it. If you may need to swap RabbitMQ for another broker, use a transport-agnostic framework instead; that is a real feature and protobus does not have it.
Protocol Buffers, not JSON
- Usually smaller on the wire — field numbers and packed integers rather than text; how much smaller depends on the payload.
- Contract-first — a
.protofile is the interface between teams, and generated typing (protobus generate) tells you when the two drift apart. - Versioning by field number — adding a field does not break an old peer.
Two runtime dependencies
aiormq for AMQP and
protobuf for the wire. The messaging
behaviour that would be hardest to reimplement — queueing, consumer
distribution, retry delays, dead-lettering — is RabbitMQ's.
Streaming, cancellation, events, priority
# A server-streaming rpc (`returns (stream Token)`) is an async generator.
async def generate(self, request, actor, correlation_id, context):
for i, word in enumerate(words):
if context.signal.aborted: # the caller stopped listening
return
yield {"index": i, "text": word}
# The client consumes it with `async for`; closing the stream tells the
# server to stop producing.
async with assistant.generate({"prompt": "..."}) as stream:
async for token in stream:
print(token["text"], end="")
if enough:
break
Events are published to a topic exchange and matched with * and #
wildcards; an opt-in retry ladder gives event handlers the same retry/DLQ
treatment requests get. Priority queues let a control message overtake a bulk
backlog. See Streaming,
Events and Priority.
Custom types
Protobuf's scalars do not cover everything. Register a custom type and it becomes usable as a field type in your schemas, encoded and decoded transparently:
from protobus import Context, CustomType
UuidType = CustomType(
name="uuid", # how it is written in the .proto
wire_type="string", # how it travels
encode=lambda value: str(value),
decode=lambda data: data,
py_type="str", # what generated typing calls it
)
context = Context()
# Register before init(): init() parses your .proto files, and a schema
# using `uuid` cannot be parsed until the type exists.
context.factory.register_type(UuidType)
await context.init("amqp://localhost", ["./proto"])
syntax = "proto3";
package Accounts;
message Account {
uuid id = 1;
}
BigIntType (an unsigned 256-bit integer, decoded to int) and
TimestampType (milliseconds since the epoch, decoded to a timezone-aware
datetime) ship with the library and are already registered. They are the
same wire format the TypeScript port uses.
Custom Types.
CLI
protobus generate # .proto -> Python typing (TypedDict / Protocol)
protobus generate:service Name # a runnable service stub from Name.proto
protobus init # print project setup instructions
Configured from pyproject.toml:
[tool.protobus]
proto_dir = "./proto"
types_output = "./types/proto.py"
services_dir = "./services"
All three keys are optional; the defaults above are what the CLI uses. CLI reference.
Documentation
Full index: docs/
| Start | |
|---|---|
| Getting Started | zero to a working RPC, plus events |
| Schema Design | writing the .proto that is your contract |
| Events | publish/subscribe and wildcard topics |
| Error Handling | retriable vs terminal, the retry ladder, the DLQ |
| Testing | unit, integration and end-to-end |
| Understand it | |
|---|---|
| Architecture | what a service creates in the broker, and why |
| Message Flow | the wire format and the round trip |
| Delivery Guarantees | acks, confirms, duplicates, the parked caller |
| Look it up | |
|---|---|
| Configuration | every environment variable and its default |
| API reference | Context, MessageService, RunnableService, ServiceProxy |
| Errors | every exported error class and when it is raised |
| Custom Types | extending the type system |
| Run it | |
|---|---|
| Troubleshooting | symptom, cause, fix |
| Security | what actor does and does not prove |
| Logging | levels, structured records, your own sink |
| Queue Migration | changing settings on live queues |
| Known Issues | current limitations |
| Migration Guide | upgrading from 1.x to 2.0 |
See a real system in a minute
git clone https://github.com/ArielLaub/protobus-py && cd protobus-py
python -m venv venv && venv/bin/pip install -e ".[dev]"
docker compose up -d
PYTHON=$PWD/venv/bin/python scripts/run-combat-sample.sh
Six services fight a battle royale over the bus — RPC, published events and
graceful shutdown in one run — and the script asserts exactly one player
survived. The source is sample/combatGame. A second
sample, sample/tokenStream, streams tokens like an LLM
and shows cancellation actually stopping the producer.
Requirements
- Python 3.10+ (CI runs 3.10 – 3.13)
- RabbitMQ 3.8+
Development
python -m pytest tests/unit # unit suite, no broker needed
docker compose up -d
python -m pytest tests/integration # against RabbitMQ (management API at :15672 for the recovery tests)
PYTHON=$PWD/venv/bin/python scripts/run-combat-sample.sh
The cross-language suite (tests/integration/test_cross_language.py) expects
the TypeScript checkout built beside this repo, or PROTOBUS_TS pointing at
it; the mirror test in that repo drives a Python server from a TypeScript
client.
License
MIT. See LICENSE.
Metadata
Release files for protobus 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| protobus-2.0.0.tar.gz | 103.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| protobus-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 216.3 kB
Release files / protobus-2.0.0.tar.gz
| Download URL | protobus-2.0.0.tar.gz |
|---|---|
| Size | 103.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
af6d8a888035d6ec3a60252b32ee6118518ef7e11bafbfea0adc834afc80c46a
|
|
BLAKE2b-256 checksum How to use checksums |
e55954c9f9e3e42caca43a0455b93344a3bf3ce360ce9f58a730abdc47d04f1a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / protobus-2.0.0-py3-none-any.whl
| Download URL | protobus-2.0.0-py3-none-any.whl |
|---|---|
| Size | 112.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8273def11712c2ad5eb7be7b9e5eabe4abb80563bc62728e141933f14eb6bc1e
|
|
BLAKE2b-256 checksum How to use checksums |
314769ac296111f5ed74e7e59a26a54bbde69035ea5929185dd393a98a35a3cc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log