# RPC Demystified

## Blog Details

- **Author**: Naveen R.
- **Date**: November 28, 2025
- **Tags**: RPC, gRPC, distributed systems
- **Read Time**: 10 mins

So you're building distributed systems and keep hearing about RPC, gRPC, and all these fancy communication protocols. Maybe you're wondering if REST APIs are enough, or if you should jump on the RPC bandwagon. Let me break it down for you in a way that actually makes sense.

## What's All the Fuss About RPC?

Think of RPC (Remote Procedure Call) like making a phone call to your friend. You dial their number, ask them to do something, wait for their response, and then continue with your day. That's essentially what RPC does for your services, it lets one service call functions on another service as if they were local functions.

![RPC request lifecycle flow](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/rpc-demystified-why-every-developer-should-care-about-remote-procedure-calls-in-2024/m1.svg)

The beauty of RPC is in its simplicity. Instead of crafting HTTP requests with headers, status codes, and JSON payloads, you just call `userService.getUser(123)` and get back a user object. The networking complexity? Hidden away.

## But Wait, How Does This Actually Work?

Here's where it gets interesting. RPC isn't magic, it's just really good abstraction. When you make that `getUser(123)` call, here's what happens under the hood:

1. **Client Stub** intercepts your function call
2. **Marshaling** converts your parameters into a network-friendly format
3. **Network Transport** sends the data to the remote service
4. **Server Stub** receives and unmarshals the data
5. **Actual Function** executes on the server
6. **Response Journey** happens in reverse


The client and server stubs are the real MVPs here. They handle all the serialization, network communication, and error handling so your application code stays clean.

## gRPC: The Modern RPC That Actually Delivers

Now, let's talk about gRPC, because this is where RPC gets really exciting. Google built gRPC to solve real problems that developers face every day.

### HTTP/2: Not Your Grandfather's HTTP

Remember the days of HTTP/1.1 where each request needed its own connection? gRPC uses HTTP/2, which means:

- **Multiplexing**: Multiple requests over one connection
- **Header Compression**: Less bandwidth waste
- **Server Push**: Servers can send multiple responses

![HTTP/2 multiplexing sequence](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/rpc-demystified-why-every-developer-should-care-about-remote-procedure-calls-in-2024/m2.svg)

### Protocol Buffers: Serialization That Doesn't Suck

JSON is great for humans, terrible for machines. Protocol Buffers (protobuf) are the opposite:

- **Smaller**: Binary format means less network traffic
- **Faster**: Serialization/deserialization is lightning quick
- **Typed**: Catch errors at compile time, not runtime
- **Language Agnostic**: Same schema works across different languages

Here's what a simple protobuf definition looks like:

```protobuf
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  repeated string roles = 4;
}

message GetUserRequest {
  int32 user_id = 1;
}
```

### Streaming: Because Sometimes You Need More Than Request-Response

gRPC supports four types of communication:

1. **Unary**: Traditional request-response
2. **Server Streaming**: One request, multiple responses
3. **Client Streaming**: Multiple requests, one response  
4. **Bidirectional Streaming**: Multiple requests and responses


This is huge for real-time applications. Imagine building a chat service where messages flow both ways continuously, that's bidirectional streaming in action.

## RPC vs REST: The Eternal Debate

Let's settle this once and for all. It's not about which is "better", it's about which fits your use case.

### When REST Makes Sense

REST shines when you're dealing with:
- **Public APIs** that need to be discoverable
- **CRUD operations** on resources
- **Caching** requirements (HTTP caching works great)
- **Stateless** interactions

### When RPC Makes Sense

RPC is your friend when you need:
- **High performance** internal communication
- **Type safety** across service boundaries
- **Complex operations** that don't map to HTTP verbs
- **Streaming** data

![API selection decision flow](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/rpc-demystified-why-every-developer-should-care-about-remote-procedure-calls-in-2024/m3.svg)

## But What About Message Queues and GraphQL?

Good question. Each communication pattern solves different problems:

### RPC vs Message Queues

**RPC** is synchronous, **Message Queues** are asynchronous:

- Use RPC when you need immediate responses
- Use message queues when you can wait and want decoupling

### RPC vs GraphQL

**GraphQL** is about flexible data fetching, **RPC** is about remote function calls:

- GraphQL: "Give me user data, but only name and email"
- RPC: "Execute this specific function with these parameters"

### RPC vs WebSockets

**WebSockets** maintain persistent connections, **RPC** can work over various transports:

- WebSockets for real-time bidirectional communication
- RPC for structured service-to-service calls

## The Dark Side: RPC Challenges You Need to Know

RPC isn't all sunshine and rainbows. Here are the gotchas:

### Network Latency is Real

Every RPC call goes over the network. That means:
- **Latency** adds up with multiple calls
- **Network failures** can break your application
- **Timeouts** need careful configuration

### The Coupling Problem

RPC can create tight coupling between services:
- Changes to function signatures break clients
- Service dependencies become complex webs
- Versioning becomes critical

### Debugging Distributed Systems is Hard

When an RPC call fails, where's the problem?
- Client code?
- Network?
- Server code?
- Load balancer?


## Security: Don't Let Your RPCs Become Attack Vectors

gRPC comes with built-in security features, but you still need to think about:

### Authentication and Authorization

```go
// Example: Adding authentication to gRPC
conn, err := grpc.Dial("localhost:50051", 
    grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
    grpc.WithPerRPCCredentials(&tokenAuth{token: "your-jwt-token"}))
```

### TLS Encryption

Always use TLS in production. gRPC makes this easy:
- Built-in TLS support
- Certificate-based authentication
- Mutual TLS for service-to-service communication

## Performance: Making Your RPCs Fly

Here are some tricks to squeeze every bit of performance out of your RPC calls:

### Connection Pooling

Don't create new connections for every call:

```python
# Bad: New connection every time
def get_user(user_id):
    channel = grpc.insecure_channel('localhost:50051')
    stub = UserServiceStub(channel)
    return stub.GetUser(GetUserRequest(user_id=user_id))

# Good: Reuse connections
channel = grpc.insecure_channel('localhost:50051')
stub = UserServiceStub(channel)

def get_user(user_id):
    return stub.GetUser(GetUserRequest(user_id=user_id))
```

### Batch Operations

Instead of multiple individual calls, batch them:

```protobuf
// Instead of multiple GetUser calls
rpc GetUsers(GetUsersRequest) returns (GetUsersResponse);

message GetUsersRequest {
  repeated int32 user_ids = 1;
}
```

### Async/Non-blocking Calls

Don't block your threads waiting for responses:

```javascript
// Async gRPC call in Node.js
const call = client.getUser({user_id: 123}, (error, response) => {
  if (error) {
    console.error('RPC failed:', error);
  } else {
    console.log('User:', response);
  }
});
```

## Service Discovery: How Services Find Each Other

In a microservices world, services need to find each other. Here are common patterns:

### DNS-Based Discovery

Simple but effective:
- Services register with DNS
- Clients resolve service addresses via DNS lookups
- Works with existing infrastructure

### Service Registry

More sophisticated approach:
- Central registry (like Consul or etcd)
- Services register themselves
- Clients query registry for service locations

![Service discovery architecture flow](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/rpc-demystified-why-every-developer-should-care-about-remote-procedure-calls-in-2024/m4.svg)

## Load Balancing: Spreading the Load

gRPC supports multiple load balancing strategies:

### Client-Side Load Balancing

The client decides which server to call:
- **Round Robin**: Distribute calls evenly
- **Weighted Round Robin**: Some servers get more traffic
- **Least Request**: Send to server with fewest active requests

### Server-Side Load Balancing

A load balancer sits between client and servers:
- **L4 Load Balancing**: Based on IP and port
- **L7 Load Balancing**: Based on gRPC method names

## Monitoring and Observability: Know What's Happening

You can't manage what you can't measure. For RPC systems, track:

### Key Metrics

- **Request Rate**: Requests per second
- **Error Rate**: Failed requests percentage  
- **Latency**: Response time distribution
- **Saturation**: Resource utilization

### Distributed Tracing

Follow requests across multiple services:

```go
// Example: Adding tracing to gRPC
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"

conn, err := grpc.Dial("localhost:50051",
    grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()))
```


## Testing RPC Services: Don't Ship Broken Code

Testing distributed systems is tricky, but here are strategies that work:

### Unit Testing with Mocks

Mock your RPC clients:

```python
# Python example with mock
from unittest.mock import Mock

def test_user_service():
    mock_client = Mock()
    mock_client.GetUser.return_value = User(id=1, name="John")
    
    service = UserService(mock_client)
    user = service.get_user(1)
    
    assert user.name == "John"
```

### Integration Testing

Test actual RPC communication:

```go
// Go example with test server
func TestUserService(t *testing.T) {
    // Start test gRPC server
    lis, _ := net.Listen("tcp", ":0")
    s := grpc.NewServer()
    RegisterUserServiceServer(s, &testUserService{})
    go s.Serve(lis)
    
    // Test client
    conn, _ := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
    client := NewUserServiceClient(conn)
    
    resp, err := client.GetUser(context.Background(), &GetUserRequest{UserId: 1})
    assert.NoError(t, err)
    assert.Equal(t, "John", resp.Name)
}
```

## Real-World Implementation: A Practical Example

Let's build a simple user service to see RPC in action:

### 1. Define the Protocol

```protobuf
syntax = "proto3";

package user;

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (stream User);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  int64 created_at = 4;
}

message GetUserRequest {
  int32 id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
}
```

### 2. Implement the Server

```go
type userServer struct {
    users map[int32]*User
    nextID int32
}

func (s *userServer) GetUser(ctx context.Context, req *GetUserRequest) (*User, error) {
    user, exists := s.users[req.Id]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "user not found")
    }
    return user, nil
}

func (s *userServer) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
    s.nextID++
    user := &User{
        Id:        s.nextID,
        Name:      req.Name,
        Email:     req.Email,
        CreatedAt: time.Now().Unix(),
    }
    s.users[user.Id] = user
    return user, nil
}
```

### 3. Create the Client

```go
func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    client := NewUserServiceClient(conn)
    
    // Create a user
    user, err := client.CreateUser(context.Background(), &CreateUserRequest{
        Name:  "John Doe",
        Email: "john@example.com",
    })
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Created user: %v\n", user)
    
    // Get the user
    getResp, err := client.GetUser(context.Background(), &GetUserRequest{
        Id: user.Id,
    })
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Retrieved user: %v\n", getResp)
}
```

## Error Handling: When Things Go Wrong

RPC calls can fail in many ways. Here's how to handle them gracefully:

### gRPC Status Codes

gRPC uses standard status codes:

```go
import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"

func handleError(err error) {
    if err != nil {
        st, ok := status.FromError(err)
        if ok {
            switch st.Code() {
            case codes.NotFound:
                fmt.Println("Resource not found")
            case codes.PermissionDenied:
                fmt.Println("Access denied")
            case codes.Unavailable:
                fmt.Println("Service unavailable, retry later")
            default:
                fmt.Printf("RPC failed: %v\n", st.Message())
            }
        }
    }
}
```

### Retry Logic

Implement smart retries:

```python
import grpc
from grpc import StatusCode
import time

def call_with_retry(stub_method, request, max_retries=3):
    for attempt in range(max_retries):
        try:
            return stub_method(request)
        except grpc.RpcError as e:
            if e.code() == StatusCode.UNAVAILABLE and attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
                continue
            raise
```

## The Future of RPC: What's Coming Next

RPC technology keeps evolving. Here's what to watch:

### HTTP/3 and QUIC

gRPC is exploring HTTP/3 support:
- **Faster connection establishment**
- **Better handling of packet loss**
- **Improved mobile performance**

### WebAssembly Integration

WASM could change how we deploy RPC services:
- **Language-agnostic deployment**
- **Better resource isolation**
- **Edge computing possibilities**

### AI-Powered Code Generation

Tools are getting smarter at generating RPC code:
- **Automatic client generation**
- **Smart error handling**
- **Performance optimization suggestions**

## Making the Decision: Should You Use RPC?

Here's a decision framework:

![Service communication decision flow](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/rpc-demystified-why-every-developer-should-care-about-remote-procedure-calls-in-2024/m5.svg)

### Use RPC When:
- Building internal microservices
- Performance is critical
- You need type safety
- Streaming is required
- Your team is comfortable with the complexity

### Stick with REST When:
- Building public APIs
- Simplicity is more important than performance
- You need HTTP caching
- Your team prefers familiar patterns

## Wrapping Up: RPC in the Real World

RPC isn't just another buzzword, it's a powerful tool for building distributed systems. When used correctly, it can make your services faster, more reliable, and easier to maintain.

The key is understanding when to use it. RPC shines in internal service communication where performance and type safety matter. It's not always the right choice for public APIs or simple CRUD operations.

Start small. Pick one internal service communication path and try gRPC. See how it feels. Measure the performance difference. Get comfortable with the tooling. Then decide if it's right for your broader architecture.

Remember, the best architecture is the one your team can build, maintain, and debug effectively. RPC is a powerful tool, but it's just one tool in your toolkit.

## What's Next?

Want to dive deeper? Here are some next steps:

1. **Try the gRPC quickstart** in your favorite language
2. **Build a simple service** using the examples above  
3. **Measure performance** compared to your current REST APIs
4. **Experiment with streaming** for real-time features
5. **Set up monitoring** to understand RPC behavior in production

The future of distributed systems is exciting, and RPC is a big part of that future. Whether you're building the next unicorn startup or maintaining enterprise systems, understanding RPC will make you a better developer.

Now go build something awesome.

---

*Have questions about RPC implementation? Hit me up in the comments. I love talking about distributed systems architecture and helping developers make better technology choices.*
