pk.org: CS 417/Lecture Notes

Using Remote Procedure Calls

XML-RPC, RPyC, and gRPC in Python

Paul Krzyzanowski – 2026-02-05

Introduction

In class, we introduced remote procedure calls as a mechanism to invoke functions on remote machines while hiding the complexity of network communication.

Now let’s look at some examples of implementing the same simple service using three different approaches, each with different trade-offs.

What We Will Build

We will build a simple calculator service that performs basic arithmetic operations remotely. A client will call functions like add(5, 3) or multiply(7, 4) on a remote server and receive results, all while the network communication, serialization, and deserialization happen transparently.

These examples are intentionally simple. The goal is not to build something useful but to understand RPC mechanics without business logic getting in the way. You will see what happens when you call a remote procedure: how data is marshalled, sent over the network, unmarshalled, and returned.

By going through these examples, we will see:

We will implement the service using:

  1. XML-RPC: The simplest approach, using Python’s standard library

  2. RPyC: A pure-Python RPC system supporting remote objects

  3. gRPC: Brief comparison to the production-grade approach (covered in the other recitation)

XML-RPC is the only framework that comes standard with Python, but others have been developed and used. We’ll look at RPyC as an example that appears to be reasonably mature and well used, has more robust features, and is natively Python. The caveat is that it is a Python-only implementation and doesn’t support communicating with non-Python software (e.g., a client written in Python and a server written in Go).

The Code

You can download a zip file containing all the demo code here.

Run:

unzip python-rpcdemo.zip

to extract the contents. This will create three directories under python-rpcdemo:

  1. xmlrpc: the XML-RPC demo

  2. rpyc: the RPyC demo

  3. grpc: the gRPC demo

Example 1: XML-RPC

XML-RPC is one of the first RPC systems developed with web services in mind. It uses XML to encode data and HTTP to transport messages. Python includes XML-RPC support in the standard library, so no external dependencies are required.

The Server

Many RPC frameworks (ONC RPC, DCE RPC, Java RMI, for example) use a name server to register a service and its port number. When a service starts up, it binds to a randomly assigned port number and registers its name and port with the name server. Before making remote procedure calls, a client will use a library function in the framework to contact the name server via a socket interface to look up the interface’s name and get the port number. The name server is expected to run on a predefined port that the client knows about (e.g., 1099 for Java’s rmiregistry, 111 for ONC RPC’s rpcbind).

RPC frameworks that focus on web services do not do this because the expectation is that web servers run on predefined ports (typically port 443 for https).

Create xmlrpc_server.py:

from xmlrpc.server import SimpleXMLRPCServer
import logging

# Configure logging to show timestamps and severity levels    
# format: controls the output format of log messages   
#   %(asctime)s - timestamp when the log was created   
#   %(levelname)s - severity level (INFO, WARNING, ERROR, etc.)   
#   %(message)s - the actual log message    
# level: only show messages at INFO level or higher (INFO, WARNING, ERROR, CRITICAL)

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)  # Create a logger for this module

class Calculator:
    """A simple calculator providing arithmetic operations"""

    def add(self, a, b):
        """Add two numbers"""
        logger.info(f"add({a}, {b})")
        return a + b

    def subtract(self, a, b):
        """Subtract b from a"""
        logger.info(f"subtract({a}, {b})")
        return a - b

    def multiply(self, a, b):
        """Multiply two numbers"""
        logger.info(f"multiply({a}, {b})")
        return a * b

    def divide(self, a, b):
        """Divide a by b"""
        logger.info(f"divide({a}, {b})")
        if b == 0:
            logger.error("Divide by zero")
            raise ValueError("Cannot divide by zero")
        return a / b

def main():
    # Create server
    server = SimpleXMLRPCServer(('localhost', 8000), logRequests=True)
    logger.info("XML-RPC server listening on port 8000")

    # Register the calculator instance
    # The instance's public methods become remotely callable
    server.register_instance(Calculator())

    # Register a function to shut down gracefully
    server.register_function(lambda: server.shutdown(), 'shutdown')

    # Start serving requests
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        logger.info("Shutting down server")


if __name__ == '__main__':
    main()

The Client

Create xmlrpc_client.py:

from xmlrpc.client import ServerProxy


def main():
    # Connect to the server
    # ServerProxy creates a proxy object that forwards method calls to the server
    server = ServerProxy('http://localhost:8000')

    print("XML-RPC Calculator Client")
    print("=" * 40)

    # Call remote methods
    # These look like local method calls but execute on the server
    result = server.add(5, 3)
    print(f"add(5, 3) = {result}")

    result = server.subtract(10, 4)
    print(f"subtract(10, 4) = {result}")

    result = server.multiply(7, 6)
    print(f"multiply(7, 6) = {result}")

    result = server.divide(15, 3)
    print(f"divide(15, 3) = {result}")

    # Error handling
    print("\nTesting error handling:")
    try:
        result = server.divide(10, 0)
        print(f"divide(10, 0) = {result}")
    except Exception as e:
        print(f"Error: {e}")


if __name__ == '__main__':
    main()

Running the Example

In one terminal, start the server:

python xmlrpc_server.py

In another terminal, run the client:

python xmlrpc_client.py

You should see:

XML-RPC Calculator Client
========================================
add(5, 3) = 8
subtract(10, 4) = 6
multiply(7, 6) = 42
divide(15, 3) = 5.0

Testing error handling:
Error: <Fault 1: "<class 'ValueError'>:Cannot divide by zero">

The server terminal shows:

2026-02-05 18:18:36,812 [INFO] XML-RPC server listening on port 8000
2026-02-05 18:18:42,385 [INFO] add(5, 3)
127.0.0.1 - - [05/Feb/2026 18:18:42] "POST /RPC2 HTTP/1.1" 200 -
2026-02-05 18:18:42,386 [INFO] subtract(10, 4)
127.0.0.1 - - [05/Feb/2026 18:18:42] "POST /RPC2 HTTP/1.1" 200 -
...

What Is Happening?

When you call server.add(5, 3), here is what happens:

  1. Client side: The ServerProxy object intercepts the method call. It does not have an add method. Instead, it uses Python’s __getattr__ magic method to catch any method call and treat it as an RPC request. The __getattr__ method is a special method used to dynamically handle access to attributes that are not found through normal lookup mechanisms. It acts as a fallback method, called only after all other ways of finding an attribute have failed.

  2. Marshalling: The method name (add) and parameters (5, 3) are encoded into XML:

    <?xml version='1.0'?>
    <methodCall>
        <methodName>add</methodName>
        <params>
            <param><value><int>5</int></value></param>
            <param><value><int>3</int></value></param>
        </params>
    </methodCall>

  3. Network transmission: This XML is sent as an HTTP POST request to http://localhost:8000.

  4. Server side: The SimpleXMLRPCServer receives the HTTP request, parses the XML, extracts the method name and parameters, and calls calculator.add(5, 3).

  5. Return marshalling: The server encodes the result (8) into XML:

    <?xml version='1.0'?>
    <methodResponse>
        <params>
            <param><value><int>8</int></value></param>
        </params>
    </methodResponse>

  6. Response transmission: This XML is sent back as an HTTP response.

  7. Client unmarshal: The client proxy parses the XML and returns the integer 8 to your code.

All of this happens transparently. From the client’s perspective, server.add(5, 3) looks like a local method call.

Understanding Python Logging

The examples use Python’s logging module to track what the server is doing. Let’s break down how it works:

Basic configuration:

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')

The level parameter sets the minimum severity to display:

Setting level=logging.INFO means DEBUG messages are hidden but INFO and above are shown.

The format parameter controls what each log line looks like. It uses a template with placeholders:

So this format:

format='%(asctime)s [%(levelname)s] %(message)s'

Produces logs like:

2026-02-05 18:18:42,385 [INFO] add(5, 3)
2026-02-05 18:18:42,388 [ERROR] Divide by zero

Creating a logger:

logger = logging.getLogger(__name__)

This creates a logger instance. The __name__ parameter is the module name (e.g., “xmlrpc_server”). This allows you to configure different modules differently and identify which module generated each log.

Using the logger:

logger.info(f"add({a}, {b})")      # INFO level message
logger.warning("Connection slow")   # WARNING level message
logger.error("Failed to parse")     # ERROR level message

In production systems, logs are typically written to files, centralized logging systems (such as Elasticsearch), or cloud logging services (such as CloudWatch or Stackdriver). The basicConfig would be replaced with more sophisticated configuration.

XML-RPC Limitations

XML-RPC is simple but has significant limitations:

Limited types: Only supports basic types (int, float, string, boolean, array, struct). No support for objects, custom classes, or complex data structures.

No type safety: The client has no way to know which methods the server supports or what parameters the server expects. Errors only appear at runtime.

Verbose: XML is human-readable but inefficient. Every integer is wrapped in <value><int>...</int></value> tags. This takes time to parse and also consumes extra bandwidth. A 64-bit int will consume 8 bytes, but even the smallest number wrapped for XML-PRC will take up 27 bytes. That alone reduces our effective network bandwidth by a factor of three!

No streaming: Only supports request-response. Cannot stream data or have bidirectional communication.

No service discovery: You must hard-code the server URL.

Despite these limitations, XML-RPC is still used for simple integrations where ease of implementation matters more than performance. It’s easy, it works, and it doesn’t require any third-party modules.

Example 2: RPyC (Remote Python Call)

RPyC is a pure-Python RPC library that supports transparent object access. Unlike XML-RPC, which only supports simple function calls, RPyC allows you to use remote objects as if they were local.

Installation

You’ll first need to install the RPyC package.

pip install rpyc --break-system-packages

The Server

Create rpyc_server.py:

import rpyc
from rpyc.utils.server import ThreadedServer
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)


class Calculator:
    """A simple calculator providing arithmetic operations"""

    def __init__(self):
        self.history = []

    def add(self, a, b):
        logger.info(f"add({a}, {b})")
        result = a + b
        self.history.append(f"{a} + {b} = {result}")
        return result

    def subtract(self, a, b):
        logger.info(f"subtract({a}, {b})")
        result = a - b
        self.history.append(f"{a} - {b} = {result}")
        return result

    def multiply(self, a, b):
        logger.info(f"multiply({a}, {b})")
        result = a * b
        self.history.append(f"{a} * {b} = {result}")
        return result

    def divide(self, a, b):
        logger.info(f"divide({a}, {b})")
        if b == 0:
            logger.error("Divide by zero")
            raise ValueError("Cannot divide by zero")
        result = a / b
        self.history.append(f"{a} / {b} = {result}")
        return result

    def get_history(self):
        logger.info("get_history()")
        return self.history


class CalculatorService(rpyc.Service):
    """RPyC service exposing calculator methods"""

    def on_connect(self, conn):
        logger.info(f"Client connected: {conn}")
        self.calculator = Calculator()

    def on_disconnect(self, conn):
        logger.info(f"Client disconnected: {conn}")

    # Expose operations directly on the service root
    def exposed_add(self, a, b):
        return self.calculator.add(a, b)

    def exposed_subtract(self, a, b):
        return self.calculator.subtract(a, b)

    def exposed_multiply(self, a, b):
        return self.calculator.multiply(a, b)

    def exposed_divide(self, a, b):
        return self.calculator.divide(a, b)

    def exposed_get_history(self):
        return self.calculator.get_history()


def main():
    logger.info("RPyC server listening on port 18861")
    server = ThreadedServer(CalculatorService, port=18861)

    try:
        server.start()
    except KeyboardInterrupt:
        logger.info("Shutting down server")


if __name__ == "__main__":
    main()

The Client

Create rpyc_client.py:

import rpyc

def main():

# This is the simple connect call if you're ok with the remote traceback

#    conn = rpyc.connect("localhost", 18861)

# This one suppresses the traceback - we still print the exception
    conn = rpyc.connect(
        "localhost",
        18861,
        config={"include_local_traceback": False},
    )

    print("RPyC Calculator Client")
    print("=" * 40)

    # The service "root" is the remote object now
    calc = conn.root

    result = calc.add(5, 3)
    print(f"add(5, 3) = {result}")

    result = calc.subtract(10, 4)
    print(f"subtract(10, 4) = {result}")

    result = calc.multiply(7, 6)
    print(f"multiply(7, 6) = {result}")

    result = calc.divide(15, 3)
    print(f"divide(15, 3) = {result}")

    print("\nCalculation history:")
    for entry in calc.get_history():
        print(f"  {entry}")

    print("\nTesting error handling:")
    try:
        result = calc.divide(10, 0)
        print(f"divide(10, 0) = {result}")
    except Exception as e:
        # print only the exception's message to avoid a "<traceback denied>" message
        msg = e.args[0] if e.args else str(e)
        print(f"Error: {msg}")

    conn.close()


if __name__ == "__main__":
    main()

Running the Example

Start the server:

python rpyc_server.py

Run the client:

python rpyc_client.py

Output:

RPyC Calculator Client
========================================
add(5, 3) = 8
subtract(10, 4) = 6
multiply(7, 6) = 42
divide(15, 3) = 5.0

Calculation history:
  5 + 3 = 8
  10 - 4 = 6
  7 * 6 = 42
  15 / 3 = 5.0

Testing error handling:
Error: Cannot divide by zero

The Key Difference: Stateful Objects

Notice that RPyC maintains state. The calculator object on the server accumulates a history across multiple method calls. Each client connection gets its own calculator instance, which persists for the duration of the connection.

This is fundamentally different from XML-RPC, which is stateless. With XML-RPC, each call is independent. The server has no way to know that multiple calls come from the same client.

RPyC creates a remote object, and objects have state. It’s a Python-based RPC system for distributed objects.

Web services, on the other hand, are generally designed to be stateless so that successive requests from the same client can be load balanced across servers and so that recovery from failure is easier (like a server rebooting).

What Is Happening?

RPyC uses a different approach than XML-RPC:

  1. Connection establishment: When you call rpyc.connect(), a TCP connection is established and a bidirectional protocol is negotiated.

  2. Object reference: conn.root.get_calculator() calls a method on the server that returns a calculator object. The server does not send the actual object. Instead, it sends an object reference (essentially a pointer or handle).

  3. Method invocation: When you call calc.add(5, 3), the client sends a message containing:

    • The object reference

    • The method name

    • The arguments

  4. Execution: The server looks up the object by reference, calls the method, and returns the result.

  5. Binary protocol: RPyC uses Python’s pickle for serialization, which is more efficient than XML but only works between Python processes.

  6. Garbage collection: When the client disconnects or the proxy is garbage collected, the server eventually cleans up the remote object (though RPyC’s garbage collection is not as sophisticated as Java RMI’s).

Observing Netref Objects

RPyC returns “netref” objects (network references). You can see this iif you add this code:

calc = conn.root.get_calculator()
print(type(calc))  # <netref class '__main__.Calculator'>
print(calc)        # <__main__.Calculator object at 0x...>

The proxy masquerades as the actual object type, but behind the scenes, every method call becomes a network message.

Synchronous vs Asynchronous

By default, RPyC calls are synchronous (blocking). The client waits for the server to respond. You can make calls asynchronous so you can send a request but wait for the results later:


# Asynchronous call returns immediately
async_result = rpyc.async_(calc.add)(5, 3)

# Do other work here...

# Get the result when ready (blocks if not ready)
result = async_result.value
print(f"Result: {result}")

This is useful when calling multiple slow services concurrently.

RPyC Advantages Over XML-RPC

Stateful objects: Remote objects can maintain state across calls.

Richer types: Supports any Python object that can be pickled, including custom classes, nested structures, and functions.

Bidirectional: The server can call methods on client-provided objects (callbacks).

Efficient: Binary pickle format is faster than XML. Pickle is a Python-specific binary serialization format used to convert object hierarchies into a byte stream for storage or transmission

Transparent: Remote objects look and behave like local objects.

RPyC Limitations

Python only: Unlik services like gRPC or Thrift, RPyC only works between Python processes. You cannot call an RPyC service from Java or JavaScript or Go.

Security: Using pickle has security implications. A malicious server could execute arbitrary code on the client.

Network transparency concerns: Making remote calls look exactly like local calls can hide performance costs. A loop that appears to operate on local data might actually make thousands of network calls.

No formal schema: There is no way to describe the service interface formally. Clients must know what methods exist through documentation or code inspection.

Example 3: gRPC

gRPC represents a modern, production-grade approach to RPC. Unlike XML-RPC and RPyC, which use dynamic proxies, gRPC requires you to define your service interface in a schema file and generate stubs before writing any code.

Installation

As with RPyC, gRPC isn’t bundled with Python, so you have to install the framework:

pip install grpcio grpcio-tools --break-system-packages

The Schema

The schema defines the data structures used over the network. This include the definition of the service.

Create calculator.proto:

syntax = "proto3";

package calculator;

// The calculator service definition
service Calculator {
  rpc Add (BinaryOperation) returns (Result);
  rpc Subtract (BinaryOperation) returns (Result);
  rpc Multiply (BinaryOperation) returns (Result);
  rpc Divide (BinaryOperation) returns (Result);
}

// A binary operation with two operands
message BinaryOperation {
  double a = 1;
  double b = 2;
}

// The result of an operation
message Result {
  double value = 1;
}

This schema is the contract between client and server. Both sides must agree on this interface. Notice:

Generate Code

Run the Protocol Buffer compiler:

python -m grpc_tools.protoc \
  -I. \
  --python_out=. \
  --grpc_python_out=. \
  calculator.proto

This generates two files:

These are the static stubs mentioned in the lecture. Unlike XML-RPC and RPyC, which generate proxies at runtime, gRPC generates them ahead of time.

The Server

Create grpc_server.py:

import grpc
from concurrent import futures
import logging

import calculator_pb2
import calculator_pb2_grpc

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)


class CalculatorServicer(calculator_pb2_grpc.CalculatorServicer):
    """Implementation of the Calculator service"""

    def Add(self, request, context):
        """Add two numbers"""
        logger.info(f"Add({request.a}, {request.b})")
        result = request.a + request.b
        return calculator_pb2.Result(value=result)

    def Subtract(self, request, context):
        """Subtract b from a"""
        logger.info(f"Subtract({request.a}, {request.b})")
        result = request.a - request.b
        return calculator_pb2.Result(value=result)

    def Multiply(self, request, context):
        """Multiply two numbers"""
        logger.info(f"Multiply({request.a}, {request.b})")
        result = request.a * request.b
        return calculator_pb2.Result(value=result)

    def Divide(self, request, context):
        """Divide a by b"""
        logger.info(f"Divide({request.a}, {request.b})")
        if request.b == 0:
            context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
            context.set_details("Cannot divide by zero")
            return calculator_pb2.Result()
        result = request.a / request.b
        return calculator_pb2.Result(value=result)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    calculator_pb2_grpc.add_CalculatorServicer_to_server(
        CalculatorServicer(), server
    )

    port = '50051'
    server.add_insecure_port(f'[::]:{port}')
    logger.info(f"gRPC server listening on port {port}")

    try:
        server.start()
        server.wait_for_termination()
    except KeyboardInterrupt:
        logger.info("Shutting down server")
        server.stop(0)


if __name__ == '__main__':
    serve()

Some of the key differences from XML-RPC and RPyC are:

Type safety: The request parameter is a BinaryOperation object with a and b fields. If you try to access a field that doesn’t exist, you get a compile-time error (or at least an IDE warning).

Explicit return types: You must return a Result object. The type system ensures you cannot accidentally return the wrong type.

Error handling: Instead of raising exceptions, you set a status code and details on the context object. This provides structured error reporting.

The Client

Create grpc_client.py:

import grpc
import calculator_pb2
import calculator_pb2_grpc


def main():
    # Create a channel to the server
    channel = grpc.insecure_channel('localhost:50051')

    # Create a stub (client)
    stub = calculator_pb2_grpc.CalculatorStub(channel)

    print("gRPC Calculator Client")
    print("=" * 40)

    # Call remote methods
    # Note: we must create message objects, not pass raw values
    result = stub.Add(calculator_pb2.BinaryOperation(a=5, b=3))
    print(f"Add(5, 3) = {result.value}")

    result = stub.Subtract(calculator_pb2.BinaryOperation(a=10, b=4))
    print(f"Subtract(10, 4) = {result.value}")

    result = stub.Multiply(calculator_pb2.BinaryOperation(a=7, b=6))
    print(f"Multiply(7, 6) = {result.value}")

    result = stub.Divide(calculator_pb2.BinaryOperation(a=15, b=3))
    print(f"Divide(15, 3) = {result.value}")

    # Error handling
    print("\nTesting error handling:")
    try:
        result = stub.Divide(calculator_pb2.BinaryOperation(a=10, b=0))
        print(f"Divide(10, 0) = {result.value}")
    except grpc.RpcError as e:
        print(f"Error: {e.code()}: {e.details()}")

    channel.close()


if __name__ == '__main__':
    main()

The key difference here: instead of calling stub.Add(5, 3), we call stub.Add(calculator_pb2.BinaryOperation(a=5, b=3)). We must create the message object explicitly. This is more verbose but provides type safety.

Running the Example

Start the server:

python grpc_server.py

Run the client:

python grpc_client.py

Output:

gRPC Calculator Client
========================================
Add(5, 3) = 8.0
Subtract(10, 4) = 6.0
Multiply(7, 6) = 42.0
Divide(15, 3) = 5.0

Testing error handling:
Error: StatusCode.INVALID_ARGUMENT: Cannot divide by zero

What Is Happening?

The gRPC flow is similar to XML-RPC but with important differences:

  1. Schema enforcement: The client cannot call a method that doesn’t exist in the .proto file. Your IDE will show an error. With XML-RPC, you only find out at runtime.

  2. Binary serialization: The BinaryOperation message is encoded using Protocol Buffers into a compact binary format, much smaller than XML.

  3. HTTP/2 transport: The message is sent over HTTP/2, which supports multiplexing (multiple concurrent calls over one connection) and binary framing.

  4. Structured errors: Instead of exceptions, gRPC uses status codes (like HTTP status codes but more comprehensive). The client can distinguish between different failure types.

Comparing Code Complexity

Let’s count the code required for each approach (I omitted comments and blank lines):

XML-RPC:

RPyC:

gRPC:

gRPC requires slightly more code and an extra build step, but provides:

gRPC Advantages Over Dynamic RPC

Type safety: Calling a non-existent method or passing the wrong types fails at compile time, not runtime.

Performance: Protocol Buffers are much faster to serialize/deserialize than XML and more compact than pickle.

Schema evolution: You can add new fields to messages without breaking old clients. Protocol Buffers handle forward and backward compatibility.

Language independence: The same .proto file generates code for Python, Java, Go, C++, JavaScript, and many other languages.

Streaming: gRPC supports streaming (not shown in this simple example) for real-time data feeds or large transfers.

Production ready: Built-in support for deadlines, cancellation, load balancing, retries, and health checking.

gRPC Disadvantages

Complexity: Requires a build step to generate code. Can’t just write code and run it like with XML-RPC.

Learning curve: More concepts to learn (Protocol Buffers, HTTP/2, status codes, metadata).

Debugging: Binary wire format is not human-readable. Need specialized tools like grpcurl (but RPyC is binary too)

Browser limitations: Native gRPC doesn’t work in JavaScript in browsers. You need to use gRPC-Web, which has limitations (limited support for streaming and the need for an intermediate proxy).

When to Use Each

XML-RPC:

RPyC:

gRPC:

From RPC Concepts to Code

Let’s connect these examples to concepts from the our lecture:

Stub Generation

XML-RPC and RPyC use dynamic stub generation: The client proxy is created at runtime using Python’s dynamic features (__getattr__ in XML-RPC, metaclasses in RPyC). This is convenient (no compilation step) but provides no compile-time type checking.

gRPC uses static stub generation: You write a .proto file and compile it to generate stubs before running your program. This requires an extra build step but catches type errors at compile time.

Trade-off: convenience vs. safety. Dynamic generation is easier to get started with. Static generation catches errors earlier and usually generates more efficient code.

Marshalling and Data Representation

XML-RPC: Uses XML Schema types. Every value is tagged with its type. This format is human-readable but verbose, leading to longer parsing times and more network bandwidth.

RPyC: Uses Python’s pickle, which preserves Python’s object structure, including types, class definitions, and references. It’s efficient but is Python-specific and potentially insecure.

gRPC: Uses Protocol Buffers with explicit schema definitions. It uses a compact binary format with schema evolution support.

Trade-off: readability vs. efficiency vs. language independence.

Handling Failures

All three examples show synchronous RPC where the client blocks waiting for a response. In production code, you may need:

Timeouts: Set a deadline for how long to wait. Without timeouts, a slow server can hang the client indefinitely.

Retries: Automatically retry transient failures. But only for idempotent operations.

Circuit breakers: Track repeated failures and stop calling a failing service to prevent cascading failures.

None of our simple examples implement these.