Skip to main content

Unary RPC

rpc Method(Request) returns (Response)

Unary RPC is the simplest pattern: the client sends one request, the server sends one response. It behaves like a synchronous function call over a persistent HTTP/2 stream. Unary RPC is the right choice for most operations – CRUD, lookups, and any request-response interaction. The server may return a gRPC status code in trailers if processing fails.

Details

Unary RPC maps directly to the mental model of a function call: client calls → server processes → server responds

HTTP/2 mechanics: the client opens a new HTTP/2 stream (DATA frame with request), the server sends a response DATA frame and then HEADERS frame (trailers) containing Grpc-Status.

Error handling: if the server returns an error, it sets Grpc-Status to a non-zero code (1–16) and an optional Grpc-Message in the trailers. HTTP status is always 200 – gRPC status lives in trailers, not HTTP status.

Deadlines: every unary call should set a deadline (context with timeout). Without a deadline, a hung server blocks the caller indefinitely. The client cancels via RST_STREAM if the deadline expires.

When to use unary: - Any CRUD operation (create user, get order, update settings) - Lookup by ID - Synchronous validation - Authentication/authorization checks - ~80% of real-world gRPC methods are unary

Proto definition

Protocol Buffer service definition
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
}

message GetUserRequest { string user_id = 1; }
message CreateUserRequest {
  string name = 1;
  string email = 2;
}
message DeleteUserRequest { string user_id = 1; }

Go server + client

Go server + client
// Server (Go)
func (s *UserService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, err := s.db.GetUser(ctx, req.UserId)
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "user %s not found", req.UserId)
    }
    return user, nil
}

// Client (Go) – set deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
user, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "usr_123"})
if err != nil {
    st, _ := status.FromError(err)
    log.Printf("code=%s msg=%s", st.Code(), st.Message())
}

When to use

CRUD operations

Lookups by ID

Validation endpoints

Auth checks

Any synchronous request-response

Backpressure

N/A – single request, single response. HTTP/2 flow control applies at the connection level.

See Also