Most LLM API clients default to REST/JSON because it is simple, but when you are shipping thousands of concurrent generations, the overhead starts to matter. grpc high throughput llm inference is not a silver bullet, but for specific workloads—streaming token floods, tight latency budgets, binary payloads—it earns its operational complexity. This guide walks the decision and the implementation path in order.
Profile the workload before touching protobuf
Don’t adopt gRPC because it feels modern. Measure first. If your service issues fewer than 50 requests per second with small prompts and full-response polling, REST over HTTP/1.1 is fine and cheaper to debug.
The cases where gRPC wins:
- Sustained token streaming to hundreds of clients from one replica.
- Large multimodal inputs (audio bytes, images) where base64 in JSON adds 33% overhead.
- Polyglot internal services where generated stubs remove hand-rolled HTTP clients.
- Need for HTTP/2 multiplexing to cut connection setup cost.
If you check two or more, proceed. Otherwise, stay on REST and spend your effort on batching.
Define a tight protobuf contract
The contract is the whole game. Model it around your actual inference call, not a generic “API” wrapper. Below is a minimal streaming chat contract.
syntax = "proto3";
package inference;
service LLM {
rpc Generate(GenerateRequest) returns (stream GenerateResponse);
}
message GenerateRequest {
string model = 1;
repeated Message messages = 2;
float temperature = 3;
int32 max_tokens = 4;
}
message Message {
string role = 1;
bytes content = 2; // raw bytes, not base64 string
}
message GenerateResponse {
bytes delta = 1; // token chunk as raw bytes
bool done = 2;
uint32 prompt_tokens = 3;
uint32 completion_tokens = 4;
}
Pitfall: putting string content for binary blobs forces encoding overhead. Use bytes. Another: versioning. Add fields with new numbers; never reuse. If you need breaking changes, make a LLMv2 service. Avoid oneof for error vs data in the same stream message unless you want to force clients to handle union decoding on every chunk—prefer a trailing status message or gRPC status codes.
Generate stubs and stand up a server
Use protoc with the grpc plugin. For Python:
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. inference.proto
A minimal server skeleton that streams synthetic tokens:
import grpc
from concurrent import futures
import inference_pb2, inference_pb2_grpc
class LLM(inference_pb2_grpc.LLMServiceServicer):
def Generate(self, request, context):
for i in range(request.max_tokens):
yield inference_pb2.GenerateResponse(
delta=f"tok{i} ".encode(),
done=(i == request.max_tokens - 1),
completion_tokens=i+1
)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=8))
inference_pb2_grpc.add_LLMServiceServicer_to_server(LLM(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
This is not production code—no auth, no model call—but it shows the shape. Real servers should asyncify the model call with asyncio or a native runtime. Blocking stubs will starve under concurrent streams.
Stream without exploding memory
gRPC server-streaming gives you an iterator. The client controls consumption speed; the server should not buffer all tokens. In Python, yield already backpressures because the framework waits for the write to drain.
Client loop:
import grpc
import inference_pb2, inference_pb2_grpc
channel = grpc.insecure_channel("localhost:50051")
stub = inference_pb2_grpc.LLMStub(channel)
req = inference_pb2.GenerateRequest(model="local", max_tokens=100)
for resp in stub.Generate(req):
if resp.delta:
print(resp.delta.decode(), end="")
if resp.done:
break
Common bug: setting grpc.max_receive_message_length too low and getting RESOURCE_EXHAUSTED on large multimodal requests. Set it explicitly:
channel = grpc.insecure_channel(
"localhost:50051",
options=[("grpc.max_send_message_length", 50*1024*1024),
("grpc.max_receive_message_length", 50*1024*1024)]
)
Propagate context and cancellation
gRPC carries deadline and cancellation via context. If a client disconnects, your server should abort the model forward instead of burning GPU cycles. In Python:
def Generate(self, request, context):
for i in range(request.max_tokens):
if not context.is_active():
return # client cancelled or deadline passed
yield inference_pb2.GenerateResponse(delta=f"tok{i} ".encode())
Ignoring context.is_active() is a classic leak. For async servers, await context.abort() or check context.cancelled().
Put a gateway in front, keep gRPC internal
Exposing raw gRPC to external developers is a support burden. Terminate external traffic as OpenAI-compatible REST, then call your replica over gRPC on the internal network. A gateway such as n4n.ai will honor client routing directives and forward cache-control hints, but the transport between your service and the model replica is where grpc high throughput llm inference pays off. Keep the edge simple; keep the hot path efficient.
If you must expose gRPC externally, ship a well-versioned proto and a fallback REST proxy.
Load test with realistic concurrency
HTTP/2 multiplexing means one TCP connection carries many streams. That is great until a single client opens 10k streams and your kernel hits somaxconn. Test with ghz:
ghz --insecure --proto inference.proto --call inference.LLM.Generate \
-d '{"model":"local","max_tokens":50}' -c 100 -n 10000 localhost:50051
Watch for:
- Stream idle timeouts (
grpc.keepalive_time_ms). - Thread pool exhaustion if you used blocking stubs.
- TLS handshake cost if you enable encryption; use mutual TLS only where required.
A typical symptom of misconfiguration is p99 latency climbing sharply once concurrent streams exceed a few thousand on a single channel—usually fixed by client-side channel pooling.
Tradeoffs you must accept
gRPC is not free.
- Debugging is harder.
curlcan’t talk to it; you needgrpcurlor custom clients. - Browser support requires grpc-web translation proxy.
- Contract drift breaks clients silently if you ignore compatibility rules.
- Smaller ecosystem of middleware compared to REST/OpenAPI.
- Protobuf encoding is cheaper than JSON parse, but it is not zero-cost; measure CPU on the serializer if you are CPU-bound.
If your team is small and your consumers are external startups, REST lowers friction. If you run a closed pipeline with high fan-out, grpc high throughput llm inference reduces CPU and latency at scale.
When to stay on REST
Use REST when:
- Payload is tiny and request rate is low.
- You need maximum interoperability with third-party tools.
- Your client base is web browsers calling directly.
You can still get some throughput wins with HTTP/2 and JSON, but you lose binary efficiency and strict typing.
Implementation checklist
- Measure RPS, payload size, streaming ratio.
- Write proto with
bytesfor blobs, versioned services. - Generate stubs, stand up async server.
- Set message size limits and keepalive.
- Front with REST gateway for external callers.
- Load test with
ghz, tune thread/event loops. - Document fallback to REST for clients that can’t do gRPC.
Following this order avoids the common mistake of building a gRPC system before knowing whether the bottleneck was transport at all.