HomeJournalThis post

ConnectRPC vs gRPC-Web for Browser Agents

Replay one agent contract through unary calls, server streams, cancellation, errors, proxies, generated clients, and observability.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

ConnectRPC vs gRPC-Web should be decided by replaying the same browser agent contract through the infrastructure you actually operate. This comparison keeps protobuf messages constant while testing unary calls, server streaming, cancellation, errors, proxies, generated clients, and trace continuity.

ConnectRPC vs gRPC-Web needs one frozen service

Define a tiny AgentSession service with StartTask as unary and WatchTask as server streaming. Freeze its protobuf schema, generated-code versions, fixture messages, deadlines, cancellation points, error cases, and metadata. ConnectRPC vs gRPC-Web is otherwise vulnerable to comparing a polished client on one side with an incomplete demo on the other. The protocol under test must carry identical meaning.

Capture the browser, client library, proxy, server adapter, TLS termination, compression setting, and tracing configuration. “Browser RPC” describes a chain, not only code running in a tab. A response can be valid at the server and still become buffered, rewritten, or stripped by an intermediary. Draw that chain before measuring so every observed limitation has an owner rather than being attributed vaguely to the protocol.

Define the task identifier and idempotency key independently. The identifier names domain work, while the key names one creation attempt; conflating them complicates retries and intentional repeated tasks.

Separate wire capability from library ergonomics

The Connect Web guide describes a protobuf web client and the Connect protocol, while the gRPC-Web repository documents its client and proxy ecosystem. Read both at pinned revisions. A library API, a wire protocol, and a particular proxy deployment are different layers; ConnectRPC vs gRPC-Web should say which layer produced each capability or constraint.

Generated-client ergonomics still matter. Compare type shapes, interceptors, deadlines, abort signals, metadata, error narrowing, tree-shaking, test doubles, and source maps using the same team conventions. Do not turn fewer setup lines into a universal architecture verdict. The better protobuf web client is the one your product can operate without hiding transport states the agent UI needs to explain.

Ask platform owners to annotate the chain with defaults they control. Compression, timeouts, buffering, header limits, and idle connection policy often decide streaming behavior before application code runs. Preserve those values with the replay receipt. That snapshot makes later transport comparisons reproducible across environments.

One protobuf message wears two web envelopesThe same StartTask and WatchTask messages pass through Connect and gRPC-Web envelopes, a production-shaped proxy, and one server contract.protobuf RPCConnectgRPC-Webproxy + server
  • Proto: shared message schema
  • Connect: one web protocol path
  • gRPC-Web: matched alternative path
  • Proxy: buffering, headers, and traces
Figure 1: Hold service meaning constant while envelope and infrastructure behavior vary.

Replay unary calls before streams

For StartTask, compare request and response bytes, content types, metadata propagation, status mapping, deadlines, abort behavior, retry ownership, and duplicate-effect prevention. A browser retry after a lost response can repeat a tool action regardless of transport. Bind every side-effecting unary call to an idempotency key and verify one ledger entry under timeout-after-commit fixtures.

Use the Protocol Buffers encoding guide to understand the message bytes, especially compatibility and field evolution. The protobuf encoding is only part of each HTTP body and envelope, so archive captured, sanitized traces from both paths. ConnectRPC vs gRPC-Web should preserve application status and error details without assuming that a successful HTTP status implies a successful agent task.

Capture request and response examples with secrets removed but framing intact for later comparison. Byte-level receipts reveal envelope and metadata differences that generated TypeScript types deliberately hide from the gRPC browser client and day-to-day callers.

The matched replay fixture models unary completion, server-stream ordering, cancellation, and typed errors at the contract level; transport adapters must produce the same receipt.

Runnable artifact — matched-browser-rpc-contract.test.mjs

import assert from "node:assert/strict";
const contract=async adapter=>{const unary=await adapter.start({id:"t7",key:"k7"});const seen=[];for await(const event of adapter.watch("t7")){seen.push(event.seq);if(event.seq===2)break}return{unary,seen,error:adapter.error({code:"permission_denied"})}};
const make=name=>({start:async x=>({id:x.id,accepted:true}),async *watch(){yield{seq:1};yield{seq:2};yield{seq:3}},error:x=>({transport:name,code:x.code,retry:false})});
for(const name of ["connect","grpc-web"]){const x=await contract(make(name));assert.deepEqual(x.unary,{id:"t7",accepted:true});assert.deepEqual(x.seen,[1,2]);assert.equal(x.error.code,"permission_denied");assert.equal(x.error.retry,false)}console.log("PASS: both RPC adapters preserve the matched contract");

Run node matched-browser-rpc-contract.test.mjs. Expected receipt: PASS: both RPC adapters preserve the matched contract.

Stress server streaming through the real proxy

WatchTask should emit ordered progress events, a terminal result, and bounded heartbeat behavior. Measure time to first event, inter-event delay, total duration, bytes, buffering, cancellation propagation, and server cleanup. Test a slow consumer and a tab moved to the background. ConnectRPC vs gRPC-Web can look identical on localhost while a production proxy buffers frames until the stream ends, destroying the product experience.

Distinguish server streaming from bidirectional streaming. A browser agent that only receives progress may need server streaming plus separate unary commands; requirements for interactive two-way control can change the transport decision entirely. The WebTransport vs WebSockets comparison helps when a full-duplex session is genuinely required. Do not imply either compared path provides capabilities excluded by the specific browser client and deployed proxy.

Expose a user-visible reconnect state when progress stalls. An agent interface that leaves an animated spinner running after the underlying stream has died converts transport ambiguity into false confidence.

TestConnectgRPC-WebOwner
Unary effectReplayReplayApp
Server streamMeasureMeasureProxy
CancellationAbortAbortAll hops
Typed errorMapMapAdapter
Trace contextVerifyVerifyPlatform
Figure 2: Score protocol behavior and operational cost as separate evidence.

Normalize errors without erasing evidence

Build one application error union: invalid_argument, unauthenticated, permission_denied, not_found, conflict, resource_exhausted, unavailable, deadline_exceeded, cancelled, and internal. Each adapter maps transport-specific status and details into that union while retaining a sanitized raw code for diagnostics. The UI decides retry and user messaging from application semantics, not from arbitrary HTTP status alone.

Compare the approach with HTTP Message Signatures in TypeScript when requests cross trust boundaries, because metadata preservation and external target reconstruction can interact with proxies. ConnectRPC vs gRPC-Web testing should include malformed details, oversized metadata, expired credentials, and a proxy-generated error page. A client must never parse HTML or an unknown binary body as a trusted protobuf error.

Preserve error causality across adapter layers. A server policy denial, proxy timeout, malformed frame, and local cancellation may share a surface category yet require different retry, alert, and support behavior. Test each category independently. Keep the originating error category in every archived support receipt.

Trace cancellation across every hop

An AbortSignal in the browser is only the first event. Confirm that the client stops reading, the proxy releases the upstream request, the server observes cancellation, model work stops when safe, and partial effects follow an explicit compensation policy. Record timestamps at each hop. If an expensive agent task continues after the tab cancels, the transport contract has failed even when the UI becomes quiet.

Test cancellation before headers, between stream frames, after server commit, and during network loss. ConnectRPC vs gRPC-Web should expose how each client represents these cases and which infrastructure metrics reveal leaked work. Do not automatically retry cancellation or deadline errors: first ask whether the operation may already have committed. Link the decision to the idempotency ledger rather than assuming transport failure equals application rollback.

Track orphaned server work as a first-class metric. Cancellation success is measured by released compute and terminal receipts, not solely by how quickly the browser removed a progress component.

  1. 1Freeze

    Pin proto, clients, proxy, and fixtures

  2. 2Replay

    Run unary, stream, error, and cancel cases

  3. 3Observe

    Correlate browser, edge, proxy, and server spans

  4. 4Choose

    Document capability, cost, and fallback

Figure 3: The matched replay follows meaning from schema to proxy and cancellation cleanup.

Include bundles, debugging, and upgrades

Measure generated and runtime bundle bytes after production minification, but include code-splitting and cache behavior. Then time a realistic debugging task: trace one permission error from browser to server and inspect one stalled stream. A few kilobytes saved can be irrelevant if the chosen stack makes field diagnosis or proxy upgrades substantially harder. ConnectRPC vs gRPC-Web is an ownership choice as much as a wire choice.

Pin compiler, generator, runtime, and proxy versions together in the fixture. Run compatibility tests when any layer changes, including unknown protobuf fields and mixed old/new clients. The agent-ready API spec template can capture auth, errors, retries, and observability that protobuf alone does not express. Preserve the losing adapter as a small conformance control if maintenance cost permits.

Include accessibility in generated-client demos by wiring stream state to readable status text and controls. Transport examples that render only a console log omit the product behavior agents actually need.

Publish the conditional transport verdict

The decision receipt should name the service, browser cohort, protocol and library versions, proxy route, unary and stream results, cancellation cleanup, error mapping, trace coverage, bundle impact, and fallback. ConnectRPC vs gRPC-Web may end with “both preserve the contract; choose Connect for direct server support” or “choose gRPC-Web for the existing Envoy estate.” Those are operationally bounded conclusions, not protocol rankings for everyone.

Revisit the choice when the interaction shape changes. OpenAPI vs AsyncAPI for agent integrations is useful if the workflow becomes event-driven rather than request/stream based. Keep the matched replay runnable so a future team can test a new proxy, client, or protocol against the same agent semantics instead of restarting the debate from anecdotes.

Rehearse rollback with mixed client generations. A safe protocol change should preserve known fields, reject incompatible semantics clearly, and never reinterpret an old request as a broader new operation.

Run the replay from a clean browser profile and from a long-lived session whose credentials, caches, and connections have aged. Record DNS, TLS, request headers, first frame, every stream frame, cancellation, terminal status, and server cleanup under one correlation ID, while redacting credentials and user content. Repeat the exercise behind the actual CDN and ingress path, because development proxies frequently have different buffering, idle timeout, compression, and header behavior. Finally, ask a developer who did not build either adapter to diagnose one injected permission error and one stalled stream from the archived traces. Their time and confidence reveal whether generated clients, proxy metrics, and error normalization create a system the wider team can own. This operational rehearsal gives the protocol decision a stronger foundation than API taste: one frozen service produced equivalent meaning, infrastructure behavior stayed visible, and the chosen stack left enough evidence for cancellation, retry, support, and future upgrades.