Demo gRPC with python, using buf and uv ty for type checking.
Find a file
pyrpc-demo c39eb3c8bb Systematic benchmarks: codec sweep 64K-2M, concurrency scaling with TCP connection counts
- 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
2026-09-08 16:08:47 +02:00
gen gRPC demo: buf codegen, all 4 RPC modes, ty type checking, benchmark 2026-09-07 21:38:05 +02:00
proto/greet/v1 gRPC demo: buf codegen, all 4 RPC modes, ty type checking, benchmark 2026-09-07 21:38:05 +02:00
.gitignore Add 2 MiB payload codec benchmark and large-payload unary throughput test 2026-09-07 22:18:11 +02:00
.python-version gRPC demo: buf codegen, all 4 RPC modes, ty type checking, benchmark 2026-09-07 21:38:05 +02:00
benchmark.py Systematic benchmarks: codec sweep 64K-2M, concurrency scaling with TCP connection counts 2026-09-08 16:08:47 +02:00
buf.gen.yaml gRPC demo: buf codegen, all 4 RPC modes, ty type checking, benchmark 2026-09-07 21:38:05 +02:00
buf.yaml gRPC demo: buf codegen, all 4 RPC modes, ty type checking, benchmark 2026-09-07 21:38:05 +02:00
client.py gRPC demo: buf codegen, all 4 RPC modes, ty type checking, benchmark 2026-09-07 21:38:05 +02:00
pyproject.toml Systematic benchmarks: codec sweep 64K-2M, concurrency scaling with TCP connection counts 2026-09-08 16:08:47 +02:00
README.md Systematic benchmarks: codec sweep 64K-2M, concurrency scaling with TCP connection counts 2026-09-08 16:08:47 +02:00
server.py gRPC demo: buf codegen, all 4 RPC modes, ty type checking, benchmark 2026-09-07 21:38:05 +02:00
uv.lock Systematic benchmarks: codec sweep 64K-2M, concurrency scaling with TCP connection counts 2026-09-08 16:08:47 +02:00

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):

  1. Codec — in-process serialize/deserialize of GreetRequest at 64 / 128 / 256 / 512 / 1024 / 2048 KiB payloads, each timed to run at least 0.5 s per direction.
  2. Unary throughput vs concurrency — 2,000 Greet round-trips at each concurrency level (1, 4, 16, 64), reporting req/s, amortized µs/req, peak TCP connections (sampled live via psutil), 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 as TCP conns: 1 at every level.
  3. Unary throughput, large payloads — 200 concurrent Greet round-trips (concurrency 8, 2 MiB payloads), reporting MB/s of request data.
  4. Bidi streaming throughput — 5,000 messages over a single GreetChat stream.

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