- Python 100%
- Codec sweep at 64K-2M with time-based measurement - Concurrency sweep with live TCP connection counts (psutil, no forking during gRPC calls) - README: methodology update matching pyrpc-demo |
||
|---|---|---|
| gen | ||
| proto/greet/v1 | ||
| .gitignore | ||
| .python-version | ||
| benchmark.py | ||
| buf.gen.yaml | ||
| buf.yaml | ||
| client.py | ||
| pyproject.toml | ||
| README.md | ||
| server.py | ||
| uv.lock | ||
pygrpc-demo
A minimal demo of a Python server and client communicating over protobuf using gRPC, with Buf for schema tooling and code generation. Everything is managed with uv — no global installs required.
This is the gRPC counterpart of pyrpc-demo (Connect RPC):
same schema, same four RPC modes, same benchmark methodology — so the two can
be compared directly.
What the pieces are
| Piece | Role in this project |
|---|---|
| uv | Python project/package manager. Owns the virtualenv and lockfile, and runs everything via uv run. |
Buf (buf-bin) |
Protobuf-native toolchain. Lints the schema and generates Python code from .proto files (via remote plugins). |
| protobuf (classic, C++/upb-backed) | The serialization format. Generated _pb2.py message classes run on google.protobuf. |
gRPC (grpcio) |
The RPC framework. HTTP/2-based, with grpc.aio for asyncio server/client. |
Dependencies (pyproject.toml)
Runtime ([project] dependencies) — what server.py and client.py import:
| Dependency | Why it's here |
|---|---|
grpcio |
gRPC runtime (server + client), including the async grpc.aio API. |
protobuf |
Classic protobuf runtime for the generated message classes. |
greet |
The generated code, packaged as a uv workspace member (see below). |
Development ([dependency-groups] dev) — tools used to build and check the
project, never imported by the running code:
| Dependency | Why it's here |
|---|---|
buf-bin |
The Buf CLI, installed inside the project's venv. Used only for uv run buf lint / uv run buf generate. |
ty |
Astral's type checker. |
psutil |
Process/socket introspection; used by benchmark.py to count live TCP connections (without forking, which gRPC dislikes). |
Project layout
proto/greet/v1/greet.proto # the schema: greet.v1.GreetService (4 RPC modes)
buf.yaml # buf module config (STANDARD lint, FILE breaking rules)
buf.gen.yaml # buf code-generation config
gen/ # generated code (uv workspace member)
pyproject.toml # makes it an installable `greet` package
greet/v1/greet_pb2.py # message classes (classic google.protobuf)
greet/v1/greet_pb2.pyi # type stubs (for ty / IDEs)
greet/v1/greet_pb2_grpc.py # gRPC service stubs (servicer + client)
server.py # gRPC server (grpc.aio)
client.py # gRPC client (grpc.aio)
benchmark.py # performance benchmark (codec + RPC throughput)
Setup
uv sync
Generate code
uv run buf lint # lint the .proto schema
uv run buf generate # write gen/greet/v1/*
buf.gen.yaml uses three Buf remote plugins:
buf.build/protocolbuffers/python— message classes (greet_pb2.py)buf.build/protocolbuffers/pyi— type stubs (greet_pb2.pyi)buf.build/grpc/python— gRPC service stubs (greet_pb2_grpc.py)
Regenerate whenever you change greet.proto.
The generated code as a package
gen/ is a uv workspace member: it has its own pyproject.toml declaring
the greet package (built with uv_build), and the root project depends on
it via tool.uv.sources (greet = { workspace = true }). This makes the
generated code importable (uv run server.py just works) and lets type
checkers resolve it as a normal installed package. (Unlike the modern
bufbuild/py plugin used in pyrpc-demo, the classic plugins don't emit
__init__.py files, so those are checked in.)
Run
Terminal 1 (server):
uv run server.py
Terminal 2 (client):
uv run client.py
Expected output:
--- unary: Greet ---
[client] Hello, world!
--- server streaming: GreetStream ---
[client] Hello #1, streamer!
[client] Hello #2, streamer!
[client] Hello #3, streamer!
--- client streaming: GreetCount ---
[client] server counted 3 requests
--- bidirectional streaming: GreetChat ---
[client] Hi, alice!
[client] Hi, bob!
[client] Hi, carol!
How the code works
Schema (proto/greet/v1/greet.proto) — identical to pyrpc-demo, all four
RPC kinds: unary Greet, server-streaming GreetStream, client-streaming
GreetCount, and bidi GreetChat. (Buf's STANDARD lint rules require
dedicated request/response messages per RPC.)
Server (server.py): subclasses the generated GreetServiceServicer,
implements all four methods as grpc.aio coroutines/generators, and serves
on 127.0.0.1:8080 with grpc.aio.server().
Client (client.py): opens a grpc.aio.insecure_channel, builds the
generated GreetServiceStub, and runs one demo per RPC mode.
Benchmark
benchmark.py measures the same things as pyrpc-demo's benchmark, for a
1:1 comparison (see pyrpc-demo's README for sample numbers and the
HTTP/1.1 vs HTTP/2 mechanics analysis):
- Codec — in-process serialize/deserialize of
GreetRequestat 64 / 128 / 256 / 512 / 1024 / 2048 KiB payloads, each timed to run at least 0.5 s per direction. - Unary throughput vs concurrency — 2,000
Greetround-trips at each concurrency level (1, 4, 16, 64), reporting req/s, amortized µs/req, peak TCP connections (sampled live viapsutil), and speedup vs concurrency 1. Where the Connect/HTTP/1.1 stack needs one connection per in-flight request, gRPC multiplexes everything over a single HTTP/2 connection — visible asTCP conns: 1at every level. - Unary throughput, large payloads — 200 concurrent
Greetround-trips (concurrency 8, 2 MiB payloads), reporting MB/s of request data. - Bidi streaming throughput — 5,000 messages over a single
GreetChatstream.
Note: the connection sampler deliberately avoids subprocesses (lsof) —
forking while gRPC's core threads are active can upset the channel.
uv run server.py # terminal 1
uv run benchmark.py # terminal 2