Designing Internal Service Boundaries with gRPC Unary RPCs
When to use unary RPCs, how to treat metadata at trust boundaries, and what to check before making it the default internal API style.
06 Aug 2026, 09:54 UTC

The decision point
Most internal API debates start with the wrong question: REST or gRPC? A more useful question is whether a unary RPC—a single request followed by a single response—is the right boundary between two services. Teams often reserve gRPC for bidirectional streaming and default everything else to JSON over HTTP, which means giving up typed contracts, per-call deadlines, and metadata propagation for no operational gain.
Useful takeaway: Use unary RPCs for request-response internal service boundaries when you need strict contracts, metadata propagation, and per-call timeouts. Keep the service definition small, treat metadata as untrusted context at the boundary, and verify behavior with grpcurl before you make it the default.
Requirements
Before choosing unary gRPC, confirm these fit:
- The interaction is request-response, not a continuous data feed.
- Both services can share or generate code from the same
.protofiles. - You need per-call deadlines and cancellation propagation.
- Authentication and tracing context travel in metadata headers and trailers.
- The call runs inside a trust zone you control, such as a VPC, service mesh, or private network.
If any condition fails—especially the last one—unary gRPC is still usable, but you need additional authorization and transport security.
Smallest suitable design
Start with one .proto file per service domain and unary methods only:
syntax = "proto3";
package inventory;
service InventoryService {
rpc GetStock(GetStockRequest) returns (GetStockResponse);
}
message GetStockRequest {
string sku = 1;
}
message GetStockResponse {
int32 quantity_on_hand = 1;
string warehouse = 2;
}
Generate client and server stubs from this file. Do not add custom framing, compression, or bidirectional streams until a measured need appears. gRPC runs over HTTP/2, so multiple unary calls share one TCP connection by default. Set a default deadline—for example, two seconds—in client configuration so a slow backend cannot hold a connection indefinitely.
Trust and data boundaries
Metadata in gRPC is key-value context carried with the call. It is not encrypted by default unless you use TLS, and it is not a substitute for authorization. Treat incoming metadata as untrusted:
- Validate tokens and tracing headers at the edge service, not inside every handler.
- Do not log raw metadata trailers if they may contain tokens or personal data.
- Enforce mTLS between services even inside the same network. A compromised neighbor should not read metadata just because it shares a subnet.
Message size is another boundary. gRPC defaults to a 4 MB maximum message size. A GetStock response is small, but a bulk export RPC can exceed the limit and fail with RESOURCE_EXHAUSTED. Compress large payloads or split them into pages before you change the default limit.
Operational checks
Use grpcurl to exercise the contract before deploying:
grpcurl -plaintext \
-d '{"sku":"ABC-123"}' \
-H 'authorization: Bearer example-token' \
localhost:50051 \
inventory.InventoryService/GetStock
Run this from a machine with network access to the server. The -plaintext flag is for local testing only; production calls should use TLS. Expected checks:
- HTTP/2 status translates to gRPC
OKand a JSON response body. - Trailing metadata contains the expected tracing or request-id header.
- Missing or malformed input returns
INVALID_ARGUMENT, not a generic HTTP 500. - A server that takes longer than the client deadline returns
DEADLINE_EXCEEDED.
Check server logs for UNIMPLEMENTED. That code usually means the client and server were compiled from incompatible .proto versions, often because a new method was added to the client but not yet deployed to the server.
Failure modes
Unary RPCs fail in predictable ways:
- Deadline exceeded: The client gives up, but the server may still be working. If the RPC is not idempotent, automatic retries can duplicate work. Make the handler idempotent or disable retries for state-changing calls.
- Unavailable: The backend is down or the connection pool is exhausted. Because HTTP/2 multiplexes many unary calls on one connection, a slow handler can block other calls on the same stream if the server is single-threaded. Tune concurrency per language runtime.
- Resource exhausted: Payloads larger than the 4 MB default, or too many concurrent requests, trigger this error. Monitor message size histograms, not just latency.
- Version mismatch: A client compiled with a newer
.protocalls a method the server does not know, producingUNIMPLEMENTED. Align deployments so the server is updated before the client, or keep old methods available during rollouts.
Conditions that change the design
Reconsider unary RPCs when:
- The server needs to push data to the client without a new request: use server streaming.
- The client and server exchange messages interleaved in real time: use bidirectional streaming, but add application-level flow control to prevent buffer bloat.
- Payloads regularly exceed 4 MB and compression does not help: consider chunked transfer or an object store with gRPC only for metadata.
- Callers are browsers or untrusted public networks: gRPC-web helps in browsers, but public endpoints often work better with REST over HTTPS and standard OAuth.
Practical verification
After deploying, confirm the boundary works:
- Call the RPC with
grpcurlusing a valid and an invalid request; verifyOKandINVALID_ARGUMENTrespectively. - Inspect trailing metadata to confirm tracing context is present and tokens are not logged.
- Force a server restart mid-call and confirm the client receives
UNAVAILABLErather than hanging. - Compile the client and server from intentionally mismatched
.protofiles in a staging environment and confirmUNIMPLEMENTEDappears, then align versions.
If all four checks pass, the unary boundary is behaving as a typed, deadline-aware contract rather than an opaque HTTP endpoint.
Limitations
gRPC over HTTP/2 can be blocked by corporate proxies that only allow HTTP/1.1. Generated stub thread safety varies by language; C++ and Java offer synchronous-style APIs, while Python uses coroutine-friendly stubs, so do not assume the same concurrency model across services. Metadata propagation preserves context across process boundaries, but it is transport metadata, not a security claim. Always verify identity and authorization at the receiving service.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.