How gRPC-Web Works
gRPC-Web is a protocol adaptation layer that brings gRPC to browser environments. Standard gRPC relies on HTTP/2 features that browsers do not expose to JavaScript, so gRPC-Web defines an alternative wire format that works over HTTP/1.1 and can be consumed by browser-based clients. It bridges the gap between gRPC's efficient binary RPC model and the constraints of web platform APIs.
This article covers why gRPC cannot run natively in browsers, how gRPC-Web solves the problem, the role of translation proxies like Envoy, the Connect protocol as a modern alternative, supported RPC patterns, TypeScript/JavaScript code generation, and how gRPC-Web compares to REST and GraphQL for frontend development.
Why gRPC Does Not Work in Browsers
gRPC was designed for server-to-server communication. It builds directly on HTTP/2 with features that the browser networking stack does not expose to application code. Three fundamental limitations prevent standard gRPC from working in a browser.
No Access to HTTP/2 Framing
gRPC sends messages as length-prefixed binary frames within an HTTP/2 stream. The protocol relies on HTTP/2's multiplexing, flow control, and binary framing layer. Browser APIs like fetch() and XMLHttpRequest operate at a higher abstraction level -- they give you the response body as a single blob or a readable stream, but they do not expose the HTTP/2 framing primitives that gRPC depends on. You cannot control individual HTTP/2 frames, manage stream priorities, or send multiple interleaved requests on a single connection from JavaScript.
No HTTP/2 Trailers
gRPC uses HTTP/2 trailers to deliver the final status code and error details after the response body has been sent. This is fundamental to how gRPC works: the server streams data, and only after the last message does it send trailing metadata containing grpc-status and grpc-message. Browser APIs do not expose HTTP trailers at all. The fetch() API provides response headers (sent before the body) but has no mechanism to read trailers (sent after the body). This means a browser client has no way to receive gRPC status information in the standard format.
No Raw TCP or HTTP/2 Connections
gRPC clients in languages like Go, Java, and C++ manage their own TCP connections and HTTP/2 sessions. They open persistent connections, multiplex streams, and handle flow control directly. Browsers forbid raw socket access for security reasons. All network access goes through browser-managed APIs that enforce CORS, TLS policies, and connection pooling. A browser cannot open a raw TCP socket to a gRPC server.
The gRPC-Web Protocol
gRPC-Web is a specification maintained by the gRPC project that defines a modified wire format compatible with browser limitations. It was designed to be as close to native gRPC as possible while working within the constraints of fetch() and XMLHttpRequest.
Wire Format Differences
The gRPC-Web wire format makes several key adaptations:
- Trailers encoded in the body -- Since browsers cannot read HTTP trailers, gRPC-Web appends the trailer metadata (including
grpc-statusandgrpc-message) as a final frame in the response body. The client distinguishes trailers from data frames by checking a flag bit in the frame header. - HTTP/1.1 compatible -- gRPC-Web requests use standard HTTP/1.1 POST requests with a
Content-Typeofapplication/grpc-weborapplication/grpc-web-text. No HTTP/2-specific features are required. - Base64 encoding option -- The
application/grpc-web-textcontent type base64-encodes the binary protobuf frames, allowing the protocol to work even in environments that cannot handle binary response bodies. The binaryapplication/grpc-webformat is more efficient and preferred when binary streaming is available. - Same length-prefixed framing -- Each message is still prefixed with a 5-byte header: 1 byte for flags (indicating data vs. trailers) and 4 bytes for the message length. This is identical to standard gRPC framing.
Frame Structure
A gRPC-Web response body consists of one or more frames, each with this structure:
+--------+----------------+----------------------------+
| 1 byte | 4 bytes | N bytes |
| flag | message length | protobuf message data |
+--------+----------------+----------------------------+
flag = 0x00: data frame (serialized protobuf message)
flag = 0x80: trailers frame (grpc-status, grpc-message, etc.)
The final frame in every response has its flag byte set to 0x80, signaling that it contains trailer metadata rather than a data message. The client reads this frame to determine whether the RPC succeeded or failed.
Content Types
gRPC-Web defines two content types:
application/grpc-web+proto-- Binary Protocol Buffer frames. Efficient and compact. This is the default mode used by most gRPC-Web clients.application/grpc-web-text+proto-- Base64-encoded frames. Each frame is individually base64-encoded. This mode is used when server-sent events or other text-only transports are needed, and it enables server streaming via chunked transfer encoding in some configurations.
The Translation Proxy: Envoy and Alternatives
Because the gRPC-Web wire format differs from native gRPC, something must translate between the two. In most deployments, a reverse proxy sits between the browser and the gRPC backend, accepting gRPC-Web requests from the browser and forwarding them as standard gRPC to the backend server.
Envoy as the gRPC-Web Gateway
The canonical deployment uses Envoy Proxy as the translation layer. Envoy has built-in support for the gRPC-Web protocol via its envoy.filters.http.grpc_web filter. When enabled, Envoy performs the following translations:
- Accepts HTTP/1.1 or HTTP/2 requests with
Content-Type: application/grpc-web - Strips the gRPC-Web framing and converts the request to a standard gRPC call over HTTP/2 to the upstream backend
- Receives the native gRPC response (with HTTP/2 trailers) from the backend
- Re-encodes the response in gRPC-Web format, embedding trailers in the response body
- Handles CORS headers so browsers can make cross-origin requests to the gRPC endpoint
Envoy also provides load balancing across multiple gRPC backend instances, health checking, circuit breaking, and observability -- making it a production-grade gRPC-Web gateway.
Other Translation Proxies
Envoy is not the only option. Several alternatives exist:
- grpc-web Go proxy -- The
grpcwebproxybinary from theimprobable-eng/grpc-webproject, a standalone Go process purpose-built for gRPC-Web translation. - Nginx -- With the
grpc_passdirective, Nginx can proxy gRPC traffic. Combined with custom configuration, it can serve as a gRPC-Web gateway, though the integration is less turnkey than Envoy. - In-process translation -- Some gRPC server frameworks (like ASP.NET Core's
Grpc.AspNetCore.Web) can handle gRPC-Web directly in the application server, eliminating the need for a separate proxy. The server middleware detects gRPC-Web requests and translates them inline.
The Connect Protocol: A Modern Alternative
The Connect protocol, developed by Buf, is a newer approach to the same problem. Rather than adapting gRPC's wire format for browsers, Connect defines a simpler HTTP-based protocol that works natively in browsers, standard HTTP clients like curl, and gRPC servers simultaneously.
How Connect Differs from gRPC-Web
Connect takes a different design philosophy:
- Unary RPCs use simple HTTP -- A Connect unary RPC is a plain HTTP POST with a JSON or binary protobuf body and a standard HTTP status code in the response. No length-prefixed framing, no custom trailers encoding. You can call a Connect API with
curland get a readable JSON response. - Streaming RPCs use a stream format -- For server streaming, Connect uses a newline-delimited envelope format that works with standard HTTP chunked transfer encoding. Each message is a JSON object with
resultorerrorfields. - No proxy required -- Connect servers handle browser requests directly. A Connect server simultaneously speaks three protocols: Connect (for browsers and curl), gRPC (for native gRPC clients), and gRPC-Web (for legacy gRPC-Web clients). No translation proxy is needed.
- Standard error model -- Connect errors are JSON objects with a
code(matching gRPC status codes by name),message, anddetails. Unlike gRPC-Web, error information is in a standard HTTP response body, not encoded in trailers.
Connect vs gRPC-Web: When to Choose Which
Connect is generally the better choice for new projects. Its advantages include: simpler debugging (curl-friendly), no proxy infrastructure, standard HTTP semantics, and better error handling. gRPC-Web remains relevant when you have an existing Envoy-based infrastructure, need strict compatibility with the official gRPC ecosystem, or are working with teams that have already standardized on the gRPC-Web toolchain.
Supported RPC Types
gRPC defines four RPC patterns, but gRPC-Web only supports two of them. This is a fundamental limitation imposed by browser HTTP APIs.
Unary RPC (Supported)
A single request followed by a single response -- the most common pattern. The client sends one message, the server processes it and returns one message. This maps directly to a standard HTTP request-response cycle and works perfectly in browsers.
// Proto definition
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
// Client call
const user = await client.getUser({ id: "usr_123" });
Server Streaming (Supported with Caveats)
The client sends one request and the server returns a stream of messages. gRPC-Web supports this, but the implementation varies by transport. With the application/grpc-web-text content type, the client can read base64-encoded chunks as they arrive using the Fetch API's ReadableStream. With the binary format, some environments buffer the entire response before making it available.
// Proto definition
service StockService {
rpc WatchPrice(WatchRequest) returns (stream PriceUpdate);
}
// Client call
for await (const update of client.watchPrice({ symbol: "AAPL" })) {
console.log(update.price);
}
Client Streaming (Not Supported)
The client sends a stream of messages followed by a single server response. This is not possible in browsers because fetch() does not support streaming request bodies in a way that gRPC-Web can use. The request body must be fully available when the request is initiated. While the Fetch API has added support for ReadableStream request bodies in some browsers, this is not yet universally available and gRPC-Web does not use it.
Bidirectional Streaming (Not Supported)
Both client and server stream messages simultaneously. This requires full-duplex communication on a single HTTP connection, which browsers do not support through standard HTTP APIs. WebSockets provide full-duplex communication, but gRPC-Web does not use WebSockets -- it stays within the HTTP request-response model.
Code Generation for TypeScript and JavaScript
Like native gRPC, gRPC-Web uses code generation from Protocol Buffer definitions to create type-safe client stubs. The code generator reads your .proto files and produces JavaScript or TypeScript classes that handle serialization, deserialization, and the gRPC-Web transport.
The Official grpc-web Generator
The official grpc-web package from the gRPC project uses protoc with the protoc-gen-grpc-web plugin. The generated code provides a service client class with methods matching your proto service definition:
// Generated from user.proto
import { UserServiceClient } from './proto/user_grpc_web_pb';
import { GetUserRequest } from './proto/user_pb';
const client = new UserServiceClient('https://api.example.com');
const request = new GetUserRequest();
request.setId('usr_123');
client.getUser(request, {}, (err, response) => {
if (err) {
console.error(err.message);
return;
}
console.log(response.getName());
console.log(response.getEmail());
});
The official generator produces code that uses getter/setter methods (like getName() and setName()) rather than plain object properties. This style is verbose and does not feel natural in TypeScript. The generated types also lack idiomatic TypeScript interfaces.
Buf and Connect-ES: Modern Code Generation
The Buf CLI and Connect-ES offer a significantly better developer experience. Connect-ES generates idiomatic TypeScript with plain object types, Promises instead of callbacks, and full type inference:
// Generated with Connect-ES from user.proto
import { createClient } from "@connectrpc/connect";
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { UserService } from "./gen/user_connect";
const transport = createGrpcWebTransport({
baseUrl: "https://api.example.com",
});
const client = createClient(UserService, transport);
// Type-safe, Promise-based, plain objects
const user = await client.getUser({ id: "usr_123" });
console.log(user.name); // string
console.log(user.email); // string
Connect-ES also supports server streaming with async iterators, interceptors for adding authentication headers, and automatic retry logic. The transport layer is pluggable -- you can switch between gRPC-Web and Connect protocol transports without changing your application code.
Buf CLI and the Buf Schema Registry
The Buf CLI (buf) replaces protoc as the tool for working with Protocol Buffers. It provides several advantages:
- Linting -- Enforces style guidelines and best practices for
.protofiles, catching issues like missing field documentation, incorrect naming conventions, and breaking changes. - Breaking change detection -- Compares the current proto definitions against a previous version and reports backward-incompatible changes. This is critical when multiple teams depend on shared proto APIs.
- Managed code generation -- The
buf generatecommand handles plugin management and code generation configuration in a singlebuf.gen.yamlfile, replacing complexprotocinvocations with shell scripts. - Dependency management -- Proto files can depend on other proto files. Buf handles dependencies through
buf.yamland can fetch them from the Buf Schema Registry (BSR).
The Buf Schema Registry (BSR) is a hosted registry for Protobuf schemas, analogous to npm for JavaScript or crates.io for Rust. Teams publish their proto definitions to the BSR, and consumers can depend on specific versions. The BSR also generates documentation, provides a web UI for browsing schemas, and can generate client SDKs on the fly in multiple languages.
# buf.gen.yaml - Code generation configuration
version: v2
plugins:
- remote: buf.build/connectrpc/es
out: src/gen
opt: target=ts
- remote: buf.build/bufbuild/es
out: src/gen
opt: target=ts
# Generate TypeScript clients
$ buf generate
CORS Considerations
Because gRPC-Web clients run in browsers, every request to a gRPC backend is subject to the browser's Cross-Origin Resource Sharing (CORS) policy. If your gRPC-Web client is served from app.example.com and your gRPC backend is at api.example.com, the browser will send a CORS preflight OPTIONS request before each gRPC-Web call.
The proxy (Envoy, Connect server, etc.) must respond to preflight requests with the appropriate headers:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, X-Grpc-Web, Authorization
Access-Control-Expose-Headers: Grpc-Status, Grpc-Message
Access-Control-Max-Age: 86400
gRPC-Web always uses HTTP POST, so GET requests cannot be used for simple CORS-exempt calls. Every gRPC-Web request triggers a preflight unless the proxy is on the same origin as the frontend. This is one advantage of the Connect protocol for unary calls -- Connect supports GET requests for idempotent methods, which can avoid CORS preflight overhead and benefit from HTTP caching.
gRPC-Web vs REST+JSON vs GraphQL
When building a web frontend that communicates with backend services, three main approaches dominate: REST with JSON, GraphQL, and gRPC-Web (or Connect). Each makes different tradeoffs.
Type Safety and Code Generation
gRPC-Web generates fully typed client code from .proto definitions. The types are derived from the schema, so the compiler catches mismatches between client and server. REST APIs typically require manually-written TypeScript types or use OpenAPI codegen, which is often incomplete. GraphQL has strong typing through its schema, and tools like GraphQL Code Generator produce TypeScript types, but the type system is different from Protobuf -- it lacks features like enums with numeric values and fixed-size integers.
Payload Size and Performance
Protocol Buffers, used by gRPC-Web, produce significantly smaller payloads than JSON. A typical protobuf message is 3-10x smaller than its JSON equivalent because field names are replaced with small integer tags and values use efficient binary encoding. For high-frequency or large-payload APIs, this can meaningfully reduce bandwidth and parsing overhead. JSON (used by REST and GraphQL) is human-readable but verbose.
API Design
REST encourages a resource-oriented design: GET /users/123, POST /orders, PATCH /users/123. gRPC uses a service-oriented design: UserService.GetUser, OrderService.CreateOrder. GraphQL uses a query-oriented design where the client specifies exactly which fields it wants. Each style suits different architectures. gRPC's service model maps well to microservice architectures where backend services already use gRPC internally.
Streaming
gRPC-Web supports server streaming, allowing the server to push a sequence of messages to the client over a single connection. REST has no native streaming support (you would use WebSockets or SSE separately). GraphQL has subscriptions, but they require a separate WebSocket transport and are not part of the core HTTP-based query model. For real-time features like live data feeds, gRPC-Web server streaming is more tightly integrated than REST alternatives.
Tooling and Ecosystem
REST has the largest ecosystem -- every HTTP client, testing tool, and CDN understands it natively. GraphQL has strong frontend tooling (Apollo, Relay, urql) with caching, optimistic updates, and pagination built in. gRPC-Web's ecosystem is smaller but growing, especially with the Buf toolchain. A key difference: gRPC-Web requests are opaque binary in the default mode, making them harder to debug in browser DevTools compared to JSON-based protocols. The Connect protocol addresses this by supporting JSON encoding.
Real-World Architecture Patterns
There are several common patterns for deploying gRPC-Web in production. The right choice depends on your existing infrastructure, team expertise, and performance requirements.
Pattern 1: Envoy Sidecar
Each gRPC service runs alongside an Envoy sidecar that handles gRPC-Web translation. This is common in Kubernetes deployments using service mesh architectures like Istio (which uses Envoy as its data plane proxy). The sidecar handles TLS termination, gRPC-Web translation, and observability. The application server only needs to implement standard gRPC.
Pattern 2: Edge Gateway
A single Envoy or API gateway at the edge of the network handles gRPC-Web translation for all backend services. The browser talks to the gateway over HTTP/1.1 or HTTP/2 with gRPC-Web encoding, and the gateway forwards requests to the appropriate backend service using native gRPC over the internal network. This centralizes the gRPC-Web configuration and CORS handling.
Pattern 3: Connect Server (No Proxy)
With the Connect protocol, the backend server handles gRPC-Web, gRPC, and Connect natively. No proxy is needed. The server uses a Connect framework (available in Go, Node.js, Java, and other languages) that speaks all three protocols. This is the simplest architecture and eliminates an entire infrastructure component.
Pattern 4: BFF (Backend for Frontend)
A dedicated Backend for Frontend service sits between the browser and the gRPC microservices. The BFF exposes a REST or GraphQL API to the browser and calls gRPC services internally. This avoids gRPC-Web entirely on the client side, at the cost of maintaining an additional service layer. This pattern is useful when the frontend needs to aggregate data from multiple gRPC services into a single response.
Setting Up a gRPC-Web Project
Here is a concrete walkthrough for setting up a gRPC-Web frontend using the modern Buf toolchain.
Step 1: Define Your Proto Schema
// proto/todo/v1/todo.proto
syntax = "proto3";
package todo.v1;
message Todo {
string id = 1;
string title = 2;
bool completed = 3;
}
message ListTodosRequest {}
message ListTodosResponse {
repeated Todo todos = 1;
}
message CreateTodoRequest {
string title = 1;
}
service TodoService {
rpc ListTodos(ListTodosRequest) returns (ListTodosResponse);
rpc CreateTodo(CreateTodoRequest) returns (Todo);
rpc WatchTodos(ListTodosRequest) returns (stream Todo);
}
Step 2: Configure Buf
# buf.yaml
version: v2
modules:
- path: proto
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/bufbuild/es
out: src/gen
opt: target=ts
- remote: buf.build/connectrpc/es
out: src/gen
opt: target=ts
Step 3: Generate Code and Use It
# Generate TypeScript stubs
$ buf generate
# Install runtime dependencies
$ npm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf
// src/client.ts
import { createClient } from "@connectrpc/connect";
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { TodoService } from "./gen/todo/v1/todo_connect";
const transport = createGrpcWebTransport({
baseUrl: "https://api.example.com",
});
const client = createClient(TodoService, transport);
// List todos
const { todos } = await client.listTodos({});
// Create a new todo
const newTodo = await client.createTodo({ title: "Write article" });
// Watch for updates (server streaming)
for await (const todo of client.watchTodos({})) {
console.log("Updated:", todo.title, todo.completed);
}
Error Handling and Status Codes
gRPC-Web uses the same status code system as native gRPC. The grpc-status trailer (embedded in the response body for gRPC-Web) carries a numeric code, and grpc-message carries a human-readable description.
Common status codes:
0 OK-- Success3 INVALID_ARGUMENT-- The client sent invalid input5 NOT_FOUND-- The requested resource does not exist7 PERMISSION_DENIED-- The caller lacks authorization13 INTERNAL-- An unexpected server error occurred14 UNAVAILABLE-- The service is temporarily unavailable (appropriate for retries)16 UNAUTHENTICATED-- The caller is not authenticated
In Connect-ES, errors are thrown as ConnectError instances with typed code properties, making error handling natural in TypeScript:
import { ConnectError, Code } from "@connectrpc/connect";
try {
await client.getUser({ id: "nonexistent" });
} catch (err) {
if (err instanceof ConnectError && err.code === Code.NotFound) {
console.log("User not found");
}
}
Performance Considerations
Several factors affect gRPC-Web performance in browser environments:
- Binary vs text encoding -- The binary
application/grpc-webformat is more compact and faster to parse than the base64grpc-web-textformat, which adds ~33% overhead. Use binary when possible. - Connection reuse -- HTTP/2 between the browser and the proxy allows multiplexing multiple gRPC-Web calls on a single connection. HTTP/1.1 limits concurrent requests to ~6 per domain.
- Proxy latency -- The translation proxy adds a hop. Measure the added latency; for most deployments it is under 1ms, but it is an additional point of failure. Connect eliminates this entirely.
- Message size -- Protobuf serialization is fast, but very large messages (multi-megabyte) still require time to serialize and deserialize in JavaScript. Consider pagination for large result sets.
- Streaming backpressure -- Server streaming in gRPC-Web does not support true backpressure in all browser implementations. If the server produces messages faster than the client can process them, messages may buffer in memory.
Security
gRPC-Web inherits the security properties of HTTPS. All gRPC-Web traffic should use TLS in production. Authentication is typically handled via metadata headers -- the gRPC equivalent of HTTP headers.
Common authentication patterns:
- Bearer tokens -- Send a JWT or session token in the
Authorizationmetadata header. Connect-ES interceptors make it easy to attach tokens to every request. - Cookie-based auth -- When the gRPC-Web endpoint is on the same domain as the frontend, cookies are sent automatically. This works well with traditional session-based auth.
- mTLS -- Mutual TLS between the proxy and backend services provides service-level authentication. The browser-to-proxy leg uses standard TLS.
When to Use gRPC-Web
gRPC-Web is the right choice when:
- Your backend services already use gRPC and you want the frontend to use the same proto contracts and type system
- You need efficient binary serialization for performance-sensitive APIs (dashboards with frequent polling, real-time data displays, mobile-optimized web apps)
- You want server streaming for live updates without maintaining a separate WebSocket infrastructure
- Your organization uses Protocol Buffers as the canonical schema definition language across all services
- You need strong forward/backward compatibility guarantees for API evolution, which Protobuf provides by design
gRPC-Web is less appropriate for public-facing APIs where ease of integration matters more than performance, or for teams without existing Protobuf infrastructure. For those cases, REST with OpenAPI or GraphQL provides a lower barrier to entry.
The Broader Ecosystem
gRPC-Web exists within a larger ecosystem of tools built around Protocol Buffers and gRPC. Understanding how these pieces fit together helps when evaluating the technology:
- Protocol Buffers -- The schema language and binary serialization format that gRPC and gRPC-Web use for message encoding.
- gRPC -- The full RPC framework for server-to-server communication over HTTP/2. gRPC-Web is a browser-compatible adaptation of this protocol.
- Connect -- A modern protocol from Buf that is gRPC-compatible but also works with plain HTTP, making it browser-native without a proxy.
- Buf CLI -- The modern replacement for
protoc, providing linting, breaking change detection, and managed code generation. - Buf Schema Registry (BSR) -- A hosted registry for publishing and consuming Protobuf schemas, with generated documentation and SDKs.
- Envoy -- The proxy/load balancer most commonly used to bridge gRPC-Web clients to native gRPC backends.
The trend is toward simplification: the Connect protocol and Buf toolchain eliminate much of the complexity that made gRPC-Web adoption difficult in its early years. New projects can adopt gRPC-Web with significantly less infrastructure overhead than was required even two years ago.