Dinesh/ Blog
systems

gRPC & Protobuf — When RPC Wins

Define RPC messages and four streaming shapes with explicit limits; Verify Protobuf bytes, presence and mixed-schema compatibility; Distinguish message framing, deadlines, cancellation and durable effects.

20 min readChecking device speech…
Lesson preparation & details

Level: intermediate

By the end, you should be able to

  • Define RPC messages and four streaming shapes with explicit limits
  • Verify Protobuf bytes, presence and mixed-schema compatibility
  • Distinguish message framing, deadlines, cancellation and durable effects

Bring with you

Sources checked: . A source check does not change the publication or update date.

Editorial review: · What review means

In this article · 35 sections

Review boundary — 8 October 2026: Wire/framing claims use the gRPC v1.66.2 protocol document and captured official Protobuf guides. The exact program executes the installed Python Protobuf runtime locally, plus bounded stream/deadline models. No protoc generation, gRPC server/client connection, TLS, proxy or benchmark was run.

Learning goal: Define messages and RPC shapes, follow generated Python artifacts, inspect actual Protobuf bytes and compatibility, and design deadlines, cancellation and bounded streams.

Why this session exists

Generated RPC contracts can reduce handwritten client glue and make message definitions shared across languages. Protobuf can reduce bytes for some shapes and provides evolution rules; gRPC adds method semantics, transport framing, metadata/status, deadlines and streaming. Neither guarantees smaller/faster payloads for every workload nor makes malformed or unauthorised requests impossible. REST/JSON can also use schemas and generated clients, and internal services do not universally use one protocol.

You will be able to write messages/services/streams, explain Python message/stub generation and unary/server-streaming reference calls, decode tags/varints/wire types, compare protocol choices, and choose among all four RPC shapes.

Prerequisites

Bring HTTP/2 concepts, Python packaging/functions and API identity/error semantics. Generated types do not replace business validation, tenant authorisation or a durable retry contract.

Optional preparation resources

Choose a resource for the question you want to investigate; there is no required viewing list or fixed pre-read duration.

(a) Intuition · 5 min

A shipping manifest is a useful analogy: both parties know what numbered fields mean, so the document need not repeat long labels. But scanners can read a correctly shaped manifest with the wrong business meaning, just as a reused Protobuf field number can silently reinterpret old data. JSON's readable keys help debugging; Protobuf's compact wire identity requires schema-aware tools for semantic interpretation.

The four RPC shapes gRPC gives you

Unary: one request and one response, such as fetching a profile. Server-streaming: one request and a response sequence, such as bounded log results or change notifications. Client-streaming: a sequence of request messages and one response, such as uploaded chunks followed by an aggregate receipt. Bidirectional: independent request and response streams, such as collaborative cursor updates. Message order is preserved within each stream, not a global application order across directions or calls.

Choose bounded unary pages when resumability/cacheability fits better than a long stream. Streaming requires lifecycle, replay/reconnect, backpressure and cancellation policies, not merely a keyword in .proto.

How gRPC got here

The architectural lineage is from schema-based RPC systems to a public HTTP/2-based protocol with generated language bindings and browser-adaptation protocols. The original “gRPC 1.0 in 2015” conflated open-source introduction with later version milestones; no such date is needed to choose a protocol. gRPC-Web and Connect are distinct browser-compatible approaches, not proof that browsers expose the native HTTP/2 framing/trailers API required by ordinary gRPC.

The build pipeline

The build pipeline
Figure text equivalent — labels and relationships
  • Node “Versioned proto contract” (square).
  • Node “protoc and matching language plugins” (square).
  • Node “Message classes” (square).
  • Node “Client stub and server interface” (square).
  • Node “Application implementation and validation” (square).
  • Node “gRPC messages over HTTP/2 streams” (square).
  • Node “Compatibility checks against deployed contract” (square).
  • “Versioned proto contract” leads to “protoc and matching language plugins”. Line: normal; ends: arrow point.
  • “protoc and matching language plugins” leads to “Message classes”. Line: normal; ends: arrow point.
  • “protoc and matching language plugins” leads to “Client stub and server interface”. Line: normal; ends: arrow point.
  • “Message classes” leads to “Application implementation and validation”. Line: normal; ends: arrow point.
  • “Client stub and server interface” leads to “Application implementation and validation”. Line: normal; ends: arrow point.
  • “Application implementation and validation” leads to “gRPC messages over HTTP/2 streams”. Line: normal; ends: arrow point.
  • “Versioned proto contract” leads to “Compatibility checks against deployed contract”. Line: normal; ends: arrow point.

Python typically gets demo_pb2.py for messages and demo_pb2_grpc.py for service bindings. Other languages have their own artifact layouts; not literally two files everywhere. Generated packages need a version/distribution policy, compiler/runtime compatibility and review. A central repository is one option, not the only valid contract registry. Never let copied proto definitions diverge silently.

Anatomy of a message

A binary record begins with a varint key (field_number << 3) | wire_type. Numbers 1–15 fit one byte with supported wire types. Wire type 0 carries varints, 1 fixed 64-bit values, 2 length-delimited strings/bytes/submessages/packed fields, and 5 fixed 32-bit values; legacy groups have other wire types. This wire type is not a full semantic type declaration.

Unsigned/signed integer encoding differs: negative int32/int64 can take ten bytes, while sint32/sint64 use ZigZag. A boolean value takes one varint byte plus its field key. Strings carry byte length then UTF-8 bytes. Field names are absent from ordinary binary payloads but matter to generated source APIs and ProtoJSON.

Implicit-presence proto3 scalar defaults are normally omitted and unset/default cannot be distinguished. Explicit optional, messages and oneofs track presence differently; repeated fields have no scalar presence. An enum zero value such as STATUS_UNSPECIFIED is a useful sentinel, but implicit zero alone does not distinguish absent from explicitly set zero. There is no universal “all Protobuf values are non-null” business guarantee.

Why Protobuf can be small

08 2a 10 01 encodes field 1 varint 42 and field 2 varint 1: four bytes for a user ID and active flag. Compare exact UTF-8 JSON bytes under a stated whitespace/key policy, not an approximate “27 bytes” claim copied across different strings. For 12345 the varint is b9 60, producing five bytes with the boolean field. These payload examples exclude gRPC envelopes, HTTP/2 frames, headers, TLS, compression and connection setup. Actual savings require measuring actual representations under equivalent conditions.

gRPC on HTTP/2

A call uses an HTTP/2 stream, conventionally a POST path such as /demo.Greeter/SayHello, with content type application/grpc or a supported suffix. Each length-prefixed message has one compression flag byte and four big-endian length bytes, followed by the message bytes. A message can span DATA frames, and one DATA frame can contain multiple messages. A Python yield is one application message, not necessarily one HTTP/2 DATA frame.

Responses carry initial metadata and message content, then final gRPC status in trailers (or a trailers-only response). grpc-status is required even for OK; HTTP status 200 alone is not RPC success. grpc-message is optional explanatory text, not a replacement for structured status handling. An application can use metadata and richer error details according to the API contract. A generic HTTP proxy may not interpret these semantics correctly.

When to reach for gRPC vs REST vs GraphQL

Choose gRPC for supported generated-language clients, explicit streaming and service contracts when the operational/tooling cost is justified. Choose resource-oriented HTTP for familiar web/client/cache tooling and representations; it can use binary bodies, HTTP/2 or HTTP/3, OpenAPI and streaming responses such as SSE. Choose GraphQL for bounded client-selected graph data with query controls. These are not mutually exclusive or mandatory by “internal/public” labels.

Protobuf efficiency depends on data/runtime; HTTP multiplexing is not exclusive to gRPC. Profiling may show parsing, connection reuse, payload size or business work dominates. The original 5–10× and orders-of-magnitude claims are removed rather than presented as unmeasured benchmarks.

Deriving permanent field identity

An old sender and new receiver coexist during rolling deployment; old payloads also survive in queues, backups and databases. The receiver interprets field number 7 using its own schema. If old 7 meant user_id and new 7 means created_at, both int64, the same valid bytes acquire the wrong meaning without a parser error. Therefore reserve retired field numbers and names and do not reuse them for unrelated semantics. Enum numbers need the same discipline.

Changing a tag usually makes the old field unknown and the new one absent/default; it does not necessarily scramble another value. A wire-type mismatch may be treated as unknown, and some type changes share a wire type but truncate or reinterpret values. Follow the documented compatibility matrix rather than “every type change always throws.” Binary compatibility, generated source compatibility, ProtoJSON names and business meaning are separate dimensions.

Contract compiled into both ends

Adding an unused compatible field can preserve binary interchange when receivers handle absence/unknown fields appropriately; adding a required business expectation can still break old clients. Generated stubs do not authenticate requests, enforce application ranges or prevent a Python handler from returning an invalid object at runtime. The binary stream carries wire structure but not enough schema to recover all meanings; raw decoders can inspect field numbers/wire values without the schema, while meaningful names/types require it.

gRPC, JSON, or an edge gateway

gRPC brings codegen and operational tooling costs; JSON/HTTP brings familiar inspection and flexible representations but still needs explicit schemas/validation. A gateway can expose JSON while using gRPC internally, adding a translation hop and error/auth/streaming mapping responsibilities. A naive L4 balancer can pin one long-lived connection to one backend; alternatives include suitable L7 routing or client-side balancing across endpoints. “Every load balancer must be HTTP/2-aware” is too broad, but distribution must be verified under the actual connection pattern.

(c) Hands-on · 25 min

1. Define the schema

The original unary/server-streaming fixture remains, with an explicit bounded shouts application contract. 0 is not silently converted into one reply; the server rejects out-of-range values.

syntax = "proto3";
package demo;
message SayHelloRequest {
  string name = 1;
  int32 shouts = 2;
}
message SayHelloResponse {
  string greeting = 1;
  int32 sequence = 2;
}
service Greeter {
  rpc SayHello(SayHelloRequest) returns (SayHelloResponse);
  rpc SayHelloStream(SayHelloRequest) returns (stream SayHelloResponse);
}

2. Generate stubs

Reference only: in an authorised disposable environment with mutually compatible grpcio, grpcio-tools and Protobuf runtime, the command is:

python -m grpc_tools.protoc --proto_path=. --python_out=. --grpc_python_out=. demo.proto

No package installation or code generation occurred in this review. Check the generated runtime-version requirements before importing; do not assume any latest compiler works with any older runtime.

3. Server

This reference shows unary validation, a bounded stream, cancellation checks and cleanup. It is deliberately loopback/insecure for a future local lab; production needs supported TLS/identity and authorisation.

# service-reference: gRPC Python Greeter (not run)
from concurrent import futures
import grpc
import demo_pb2
import demo_pb2_grpc
 
class Greeter(demo_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        if not 1 <= len(request.name) <= 100:
            context.abort(grpc.StatusCode.INVALID_ARGUMENT, 'name length 1..100')
        return demo_pb2.SayHelloResponse(greeting=f'Hello, {request.name}!', sequence=0)
 
    def SayHelloStream(self, request, context):
        if not 1 <= len(request.name) <= 100 or not 1 <= request.shouts <= 100:
            context.abort(grpc.StatusCode.INVALID_ARGUMENT, 'invalid name or shouts')
        for i in range(request.shouts):
            if not context.is_active():
                return
            yield demo_pb2.SayHelloResponse(greeting=f'Hello, {request.name}!', sequence=i)
 
if __name__ == '__main__':
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
    demo_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    if not server.add_insecure_port('127.0.0.1:50051'):
        raise RuntimeError('bind failed')
    server.start()
    try:
        server.wait_for_termination()
    except KeyboardInterrupt:
        pass
    finally:
        server.stop(grace=1).wait()

4. Client

# service-reference: gRPC Python client with deadlines (not run)
import grpc
import demo_pb2
import demo_pb2_grpc
 
def main():
    with grpc.insecure_channel('127.0.0.1:50051') as channel:
        stub = demo_pb2_grpc.GreeterStub(channel)
        request = demo_pb2.SayHelloRequest(name='Dinesh', shouts=1)
        print('request payload bytes:', len(request.SerializeToString()))
        response = stub.SayHello(request, timeout=2.0)
        print(response.greeting, 'response payload bytes:', len(response.SerializeToString()))
        call = stub.SayHelloStream(demo_pb2.SayHelloRequest(name='Dinesh', shouts=3), timeout=2.0)
        try:
            for message in call:
                print(message.sequence, message.greeting)
        finally:
            call.cancel()
 
if __name__ == '__main__':
    try:
        main()
    except grpc.RpcError as error:
        print('RPC failed:', error.code())
        raise

5. Run it

A future authorised lab would run python server.py and python client.py in separate terminals, then stop the server. Here only the payload/runtime model below ran. Expected message payload lengths are request 10 bytes and response 16 bytes, not the original 17: implicit proto3 sequence=0 is omitted. The three streaming messages have sequences 0, 1, 2; nonzero sequence values add their own encoded field bytes. The request JSON {"name":"Dinesh","shouts":1} is 28 bytes; that comparison excludes all transport framing.

Exact Protobuf bytes and compatibility without a socket

This complete program constructs descriptors corresponding to the displayed message contract and uses the installed real Protobuf serializer/parser. Dynamic descriptors test the wire format without pretending protoc or gRPC transport ran.

# exact-program: protobuf_wire
import json
import struct
from google.protobuf import descriptor_pb2, descriptor_pool, message_factory
 
 
def message(name, fields):
    file = descriptor_pb2.FileDescriptorProto(name=name + '.proto', package='fixture', syntax='proto3')
    msg = file.message_type.add(name=name)
    for number, field_name, kind, optional in fields:
        f = msg.field.add(name=field_name, number=number, type=kind,
                          label=descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL)
        if optional:
            index = len(msg.oneof_decl)
            msg.oneof_decl.add(name='_' + field_name)
            f.proto3_optional = True; f.oneof_index = index
    pool = descriptor_pool.DescriptorPool(); pool.Add(file)
    return message_factory.GetMessageClass(pool.FindMessageTypeByName('fixture.' + name))
 
 
STRING = descriptor_pb2.FieldDescriptorProto.TYPE_STRING
INT32 = descriptor_pb2.FieldDescriptorProto.TYPE_INT32
Request = message('Request', [(1,'name',STRING,False),(2,'shouts',INT32,False)])
Response = message('Response', [(1,'greeting',STRING,False),(2,'sequence',INT32,False)])
Renumbered = message('Renumbered', [(1,'name',STRING,False),(5,'shouts',INT32,False)])
Reused = message('Reused', [(1,'name',STRING,False),(2,'unrelated_meaning',INT32,False)])
WrongWire = message('WrongWire', [(1,'name',STRING,False),(2,'shouts',STRING,False)])
Explicit = message('Explicit', [(1,'count',INT32,True)])
 
 
def varint(value):
    if type(value) is not int or not 0 <= value < 2**64:
        raise ValueError('unsigned 64-bit input required')
    out = bytearray()
    while value > 127:
        out.append((value & 127) | 128); value >>= 7
    out.append(value)
    return bytes(out)
 
if __name__ == '__main__':
    req = Request(name='Dinesh', shouts=1); wire = req.SerializeToString()
    expected = b'\x0a\x06Dinesh\x10\x01'
    assert wire == expected and len(wire) == 10
    response = Response(greeting='Hello, Dinesh!', sequence=0).SerializeToString()
    assert response == b'\x0a\x0eHello, Dinesh!' and len(response) == 16
    assert len(json.dumps({'name':'Dinesh','shouts':1}, separators=(',',':')).encode()) == 28
    assert Renumbered.FromString(wire).shouts == 0
    assert Reused.FromString(wire).unrelated_meaning == 1
    assert WrongWire.FromString(wire).shouts == ''  # incompatible wire field is unknown here
    absent = Explicit(); present = Explicit(count=0)
    assert not absent.HasField('count') and present.HasField('count')
    assert absent.SerializeToString() == b'' and present.SerializeToString() == b'\x08\x00'
    assert varint(12345) == b'\xb9\x60'
    framed = b'\x00' + struct.pack('>I', len(wire)) + wire
    assert len(framed) == 15 and framed[:5] == b'\x00\x00\x00\x00\x0a'
    print({'request':wire.hex(),'response':response.hex(),'grpc_message_bytes':len(framed)})

Anatomy of the setup

The proto defines message fields and service methods; generated classes serialise and parse messages and are mutable in Python, not “immutable-ish.” Service stubs/base classes connect handlers to method paths; runtime validation is still necessary. Each yielded response is an ordered message and final status determines completion. insecure_channel deliberately omits TLS for loopback demonstration; mTLS authenticates peers but not automatically the end user/document permission.

The descriptor fixture covers exact request/response fields; the service methods remain reference-only. Independent checks compare encoded bytes with hand-composed length/tag/varint oracles and sweep unsigned varints, explicit presence, renumbering, same-wire-type reuse and mismatched wire types. These are message-level observations, not end-to-end RPC compatibility certification.

Break wire compatibility and interpret the result

Move shouts from field 2 to field 5 only on one side. Old bytes at tag 2 become unknown to the new schema, and new shouts is zero. Reusing tag 2 as a different int means the old value is silently assigned a new meaning. Changing tag 2 to string uses a different wire type; the installed runtime treats the old varint as unknown in this fixture, rather than guaranteeing a mangled string or parse error. Reserve deleted tags/names, and compare old/new binary and JSON contracts before release.

(d) Production reality · 15 min

RPC deadlines, compatibility and cancellation

gRPC does not supply a universal useful default deadline. In Python the timeout argument is a duration; derive it from the remaining user-facing budget and reserve time for response/cleanup. Propagation is implementation-dependent, so verify each downstream language/client. A call that exceeds its deadline may already have committed a business effect. Cancellation tells the server the caller no longer wants the result; handlers must cooperate to stop work and release resources. It is not rollback.

# exact-program: stream_budget
 
def remaining(total_ms, elapsed_ms, reserve_ms):
    if min(total_ms, elapsed_ms, reserve_ms) < 0:
        raise ValueError('nonnegative durations required')
    return max(0, total_ms - elapsed_ms - reserve_ms)
 
 
def stream(count, active):
    if type(count) is not int or not 1 <= count <= 100:
        raise ValueError('count 1..100')
    for i in range(count):
        if not active(i):
            return
        yield i
 
if __name__ == '__main__':
    assert remaining(2000, 500, 100) == 1400
    assert remaining(2000, 1950, 100) == 0
    assert list(stream(3, lambda i: True)) == [0,1,2]
    assert list(stream(100, lambda i: i < 2)) == [0,1]
    print('bounded stream and deadline arithmetic passed')

The model has no sleeping thread or transport cancellation: it proves the explicit checks/arithmetic only. A real application must coordinate all spawned work and avoid swallowing cancellation signals.

Retries share one deadline and one logical operation

A timeout is an ambiguous observation, not evidence that nothing happened. Before retrying, classify the method, status and remaining budget. An idempotent read can often tolerate another attempt; a non-idempotent write such as order creation needs a durable operation identity bound to the caller and canonical payload before retrying, plus stored outcome/reconciliation semantics. Naturally idempotent writes can instead rely on their explicitly defined resource/update contract. Reusing the same ID with different payload must fail, not silently return the first result. A process-local dictionary does not establish durable deduplication across crashes or replicas.

The official retry guide distinguishes transparent retries from configured retry policies. gRPC may transparently retry certain failures before the server application sees the call; this does not make every business operation safely repeatable. Configured retry policies choose eligible status codes, maximum attempts and exponential backoff; response headers commit the RPC from the retry mechanism's perspective. Server pushback, retry throttling and implementation support further constrain attempts. A UNAVAILABLE result is not blanket permission for an application to invent a new operation ID. Do not layer unbounded application retries on channel retries.

For a 2,000 ms parent budget, 500 ms already spent and 100 ms reserved for returning/cleanup, the next attempt gets at most 1,400 ms. If it consumes 600 ms and backoff consumes 200 ms, at most 600 ms remains after the same reserve. Never restart the original 2,000 ms budget for each attempt. The remaining model above calculates this allowance; it does not implement transport retry scheduling, jitter, pushback or deadline propagation. When the allowance reaches zero, do not start another call. Cancellation must reach downstream work, yet neither it nor deadline expiry rolls back an already committed order.

Worked answer: after a timed-out order creation, retry or query the original logical operation according to the API's documented recovery policy. On a successful stored outcome, return that outcome rather than create another order. On an unresolved provider effect, reconcile before declaring either success or rollback. A deadline on a long-lived stream bounds the call lifetime, not automatically each application message; if you need idle or per-message deadlines, design those separately.

Presence changes the meaning of a patch

Suppose a stored counter is 7 and a patch intends to set it to 0. Under implicit proto3 scalar presence, serializing a field with value 0 omits it; ordinary merge semantics skip that default, leaving 7. With optional int32 count, setting 0 records presence, serializes the tag plus zero, and a merge can set the destination to 0. An unset optional patch still leaves the destination unchanged. For implicit fields, an API can instead define an explicit update mask; repeated fields and clear/delete operations need their own update contract.

This distinction is compatible at the wire level for many mixed schemas but can lose intent on a round trip. An explicit sender's zero passes through an implicit-presence reader; when that reader reserializes its default, the zero tag disappears. The final explicit reader now observes absence. Test the full old/new read-modify-write path, not just whether each side can parse a newly added tag. Similarly, unknown fields may survive binary parsing and reserialization but can be lost when translating through JSON or rebuilding messages field by field. Binary acceptance is not proof of preserved business meaning.

The independent CPU checks use the displayed descriptor builder to test absent/default/explicit-zero merges and this mixed-schema zero round trip. They execute the installed Protobuf runtime, not generated code or a connected gRPC service. Preserve retired numbers and names even if a fresh fixture contains no historical data.

Flow control, balancing and schema drift

Transport flow control limits transport progress; it does not cap every application queue or make a write mean “delivered to the peer.” Bound produced/queued messages and bytes, consume both directions, and avoid deadlock when both peers write indefinitely without reading. Set message-size limits according to implementation and intermediaries; a blanket five-MB upload chunk can exceed a configured receiver limit.

For long-lived calls, verify load distribution, connection failure, reconnect/resume semantics and per-document/user quotas. For schema drift, publish authoritative versioned definitions, run compatibility checks against the deployed baseline and test mixed versions/retained payloads. Buf offers lint/breaking-change/codegen/package workflows, but tooling cannot prove unchanged business meaning or reconcile an intentionally reused tag.

These are concrete failure analyses, not original unverified Netflix CPU savings or universal Google/cloud-native adoption claims. Profile the actual call path before attributing an improvement to binary encoding.

Collaborative editor design exercise

OpenDocument can be unary with an initial version; SubscribeChanges server-streaming with a resume sequence; SendChange unary with durable operation ID and version/conflict semantics; cursors can be bidirectional with bounded coalescing because old cursor positions may be obsolete. Attachments can use client-streaming bounded chunks and a final receipt/checksum, or a separately authorised upload path. Distinguish durable edits from ephemeral presence. Authenticate and authorise per document/user, enforce quotas, and specify reconnect, duplicate, ordering and conflict-resolution policy. Browser clients need a compatible edge protocol; a bidirectional native gRPC design is not automatically supported by gRPC-Web.

Where this shows up in the rest of the plan

HTTP/2 carries framing; REST can expose an edge representation; GraphQL can aggregate a gRPC backend. Service meshes/proxies have feature-specific support, and stream delivery alone does not provide event-time processing or durable exactly-once business effects. A capstone may use another protocol if it meets the requirements.

(e) Recall + stretch · 10 min

What are the generated artifacts?

In Python, message classes in demo_pb2 and service bindings in demo_pb2_grpc. Language layouts differ. Pin compatible compiler/plugins/runtime and do not hand-edit generated outputs.

Why can Protobuf be smaller?

Numbered keys replace repeated names, varints encode small values compactly and implicit defaults may be omitted. Actual size depends on field/value shape, presence, compression and transport overhead; no fixed factor is guaranteed.

Four RPC shapes and examples?

Unary profile read; server-streaming changes; client-streaming chunk upload; bidi cursors. Each still needs bounds, completion/error and reconnect semantics.

Why preserve field numbers and names?

Numbers identify ordinary binary fields; names affect generated APIs/JSON. Reuse can silently reinterpret retained bytes. Reserve retired identifiers rather than assuming old data is gone.

Which HTTP version does the native gRPC protocol use?

HTTP/2 streams, with length-prefixed messages and gRPC status trailers. One message is not one DATA frame, and HTTP 200 alone does not prove RPC success.

How do you add fields safely?

Use never-used numbers, preserve old meaning, choose presence deliberately and handle absence/unknown values. Check binary, JSON, generated API and business compatibility; not every type change has the same failure mode.

Compare schema, transport and streaming with REST.

gRPC commonly combines proto codegen with HTTP/2 and four RPC shapes. REST is an architectural style and can use OpenAPI, different representations and HTTP versions, including streaming responses. JSON is not the definition of REST.

Why browser adapters?

Browser APIs do not expose all native gRPC framing/trailer capabilities. gRPC-Web/Connect use browser-compatible protocols with their own streaming/support limits; verify the required shape rather than assuming full native bidi support.

What does Buf help with?

Schema packaging, lint, generation and configured breaking-change checks. It supports a controlled contract workflow but does not prove authorisation, business meaning or runtime interoperability by itself.

Stretch: design the collaborative editor.

Use unary initial state and durable edits, server-streaming resumable changes, bidi ephemeral cursors and bounded upload chunks as appropriate. Include document/user auth, operation IDs, version/conflict resolution, flow control, deadlines and browser-edge compatibility.

Failure exercise and worked answer

A client sees DEADLINE_EXCEEDED after creating an order. It must not assume rollback or issue a fresh logical operation ID. Query/retry the original durable identity with the same payload and bounded recovery policy; reconcile ambiguous provider effects.

Explain-out-loud test

Decode the ten-byte request, explain the sixteen-byte zero-sequence response, demonstrate tag reuse versus renumbering, and distinguish cancellation from undoing an effect.

What comes next

Authentication and authorisation determine who may invoke the contract. Continue to S059 · OAuth, OIDC and JWT. Hub: The 6-Month Learning Plan.

Suggested study route: RPC-shape intuition → build-pipeline visual → wire-format exercises and service reference → deadline/compatibility failure analysis → recall. Pause to decode the fixture bytes before moving on. The minute labels in the retained headings are planning cues, not measured completion times; running a local payload model does not complete the transport or production checks.

Sources and review boundary

gRPC v1.66.2 HTTP/2 protocol, Python basics, deadlines, cancellation, flow control, Protobuf encoding, proto3, presence, gRPC-Web 1.5.0, Buf compatibility. The final evidence records installed runtime version and exact bytes. Protoc, real RPCs, TLS, browser compatibility, load balancing and performance remain unperformed service checks.

Retry policy reference: official gRPC retry guide, access-dated for this review rather than asserted as part of the pinned v1.66.2 framing document. Presence/merge behavior uses the captured official presence guide and installed-runtime fixtures. Native retries, pushback and transport cancellation were not executed.

Pause / Recall / Apply

Can you explain it without the page?

Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.

Stored in this browser only. No account, no sync. Clearing browser data removes your record.