<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Streaming on kmcd.dev</title><link>https://kmcd.dev/tags/streaming/</link><description>Recent content in Streaming on kmcd.dev</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>All Rights Reserved</copyright><lastBuildDate>Tue, 01 Sep 2026 10:00:00 +0000</lastBuildDate><atom:link href="https://kmcd.dev/tags/streaming/index.xml" rel="self" type="application/rss+xml"/><item><title>Let's Talk About Streaming Data on the Web</title><link>https://kmcd.dev/posts/grpc-webtransport/</link><pubDate>Tue, 01 Sep 2026 10:00:00 +0000</pubDate><guid>https://kmcd.dev/posts/grpc-webtransport/</guid><description> 
                &lt;p> &lt;img hspace="5" src="https://kmcd.dev/posts/grpc-webtransport/cover_hu_60a0a0865109d2ba.webp" /> &lt;/p>
                
                A practical look at streaming data in the browser, from response streams and WebSockets to WebTransport over HTTP/3.
                </description><content:encoded><![CDATA[<p>“Streaming data” on the web might mean consuming a response as bytes arrive, receiving server events, using a long-lived bidirectional channel, or sharing one connection between independent streams. These workloads have different requirements, but we often reduce the choice to HTTP requests or WebSockets.</p>
<p>I want to look at the available options and then experiment with WebTransport. I came at this from gRPC: could WebTransport make client and bidirectional streaming practical in a browser? That question deserves its own follow-up, so I am starting with the transport.</p>
<h2 id="the-problem">The Problem</h2>
<p>The browser handles server-to-client streaming reasonably well. Things get harder when the client and server both need to send data independently, or when we need several concurrent streams without opening several connections.</p>
<p>gRPC is what led me here because its client and bidirectional streaming calls are not available through gRPC-Web. It is only one example. Chats, collaborative editors, games, live dashboards, media, and remote control interfaces all eventually run into some version of the same problem: we need data moving in both directions, often across several independent flows, but the browser does not expose a general-purpose TCP or QUIC socket.</p>
<h2 id="what-we-have-today">What We Have Today</h2>
<h3 id="fetch-streams-and-server-sent-events">Fetch Streams and Server-Sent Events</h3>
<p>For downloads, generated content, event feeds, and other server-to-client flows, streaming <code>fetch()</code> or Server-Sent Events may be enough. Fetch exposes a <code>ReadableStream</code>, while Server-Sent Events add an event-oriented channel with reconnection. Both keep ordinary HTTP semantics and remain visible in browser network tools.</p>
<div class="d2-diagram-wrapper">
    <div class="d2-diagram"
        style="display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto;max-height: 80vh; width:100%;"><img src="/d2-diagrams/56f5f40da0c2a4ab15cc5baac2a6d68e6b73a47d659ff23373a664c21154c5e7.svg" alt="D2 Diagram" loading="lazy" style="max-width: 100%; max-height: inherit; width: 100%; height: auto; object-fit: contain; display: block; margin: 0 auto;" /></div>
</div>
<p>They are not enough when the client also needs to stream data, or when both sides need to communicate independently over the same call.</p>
<h3 id="webrtc">WebRTC</h3>
<p>WebRTC gives us bidirectional media and data channels, but it was designed for communication between peers. Signaling, NAT traversal, and ICE make sense for calls and peer-to-peer applications but are awkward for a normal client-server API. It also sits outside ordinary HTTP infrastructure, making it harder to operate alongside a web backend.</p>
<h3 id="websockets">WebSockets</h3>
<p>WebSockets are the best general-purpose option we have today for bidirectional client-server communication in a browser. They are widely supported, well understood, and visible in browser developer tools. If an application needs one long-lived, ordered stream of messages, a WebSocket is often exactly the right answer.</p>
<div class="d2-diagram-wrapper">
    <div class="d2-diagram"
        style="display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto;max-height: 80vh; width:100%;"><img src="/d2-diagrams/4f408fc87bd2e5ad251964ccfae5817eff0aca53cc72ef3c10fae36ba05c8898.svg" alt="D2 Diagram" loading="lazy" style="max-width: 100%; max-height: inherit; width: 100%; height: auto; object-fit: contain; display: block; margin: 0 auto;" /></div>
</div>
<p>The problems start when we need several independent streams. We can multiplex all of them onto one WebSocket, but then we have to build a protocol with custom frames and stream IDs, plus routing, lifecycle, cancellation, backpressure, and error isolation on top of a single ordered connection. At that point, we are reimplementing a rough version of HTTP/2 in application code. It is a substantial and complex piece of infrastructure. Because every logical stream ultimately shares one ordered TCP byte stream, packet loss stalls unrelated streams until the missing data is retransmitted.</p>
<p>The other option is to open a separate WebSocket for every stream or RPC. That means another connection and another WebSocket handshake each time, which wastes resources and makes short-lived calls unnecessarily slow.</p>
<p>So WebSockets are still the best thing we have today, but they make us choose between a complicated application-level multiplexer and a pile of separate connections.</p>
<h2 id="enter-webtransport">Enter WebTransport</h2>
<p>WebTransport is a browser API for low-latency client-server communication over HTTP/3 and QUIC. Instead of giving us one ordered channel, it gives us a session that can carry multiple independent streams and datagrams:</p>
<ul>
<li>A single session can carry multiple unidirectional and bidirectional streams. Data waiting on one stream does not block the others.</li>
<li>Streams are reliable and ordered. They work well for chat messages, file transfers, application state, or an RPC that cannot lose data.</li>
<li>Datagrams are unreliable and unordered. They work well when old data has little value, such as player positions or individual frames of live audio and video.</li>
</ul>
<div class="d2-diagram-wrapper">
    <div class="d2-diagram"
        style="display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto;max-height: 80vh; width:100%;"><img src="/d2-diagrams/2cc3f715981f7d438ac8401f87534c58bf363ceb467b9ef2de9b7c5f09f40a97.svg" alt="D2 Diagram" loading="lazy" style="max-width: 100%; max-height: inherit; width: 100%; height: auto; object-fit: contain; display: block; margin: 0 auto;" /></div>
</div>
<p>This directly addresses the WebSocket tradeoff. Independent streams share one connection without sharing ordered delivery or backpressure, and we do not have to invent the multiplexing layer ourselves.</p>
<p>At a high level, this is QUIC streams exposed to JavaScript. If HTTP/2 streams already make sense to you, the model should feel familiar. The important difference is that QUIC prevents packet loss on one stream from holding up data on the others.</p>
<p>Of course, none of these capabilities help us if browsers do not expose them. Until recently, browser support was the obvious reason not to build around WebTransport.</p>
<p>That is no longer the case. <a href="https://webkit.org/blog/17862/webkit-features-for-safari-26-4/" rel="external">Safari 26.4 enabled WebTransport</a>, joining current Chrome, Edge, and Firefox releases. The version number is not a typo: Apple renumbered Safari alongside iOS, iPadOS, and macOS in 2025. The current <a href="https://caniuse.com/webtransport" rel="external">Can I Use support report</a> is green across modern major browsers. There are still old and legacy browsers without support, but browser coverage is no longer a reason to avoid testing WebTransport.</p>
<figure><a href="https://kmcd.dev/posts/grpc-webtransport/caniuse.png" class="spotlight" data-download="true" aria-label="Source: [caniuse.com/webtransport](https://caniuse.com/webtransport)">
    
    
    
    
        
            
            
                
            
            
        
    

    
    
    

    
    
        
    

    <img src="https://kmcd.dev/posts/grpc-webtransport/caniuse_hu_e1d95ae0faed167b.png"
         alt="Source: caniuse.com/webtransport"/>
    </a><figcaption>
            <p>Source: <a href="https://caniuse.com/webtransport" rel="external">caniuse.com/webtransport</a></p>
        </figcaption>
</figure>

<h2 id="browser-devtools-lack-webtransport-inspection">Browser DevTools Lack WebTransport Inspection</h2>
<p>WebTransport works. Debugging it does not. As of July 2026, none of the built-in developer tools in Chrome, Firefox, or Safari provides a WebSocket-style view of WebTransport traffic. You cannot select a session and inspect its streams or datagrams, view individual messages or frames, search their payloads, or download the exchanged application data as a response body.</p>
<p>There is no single response body for DevTools to reveal after the handshake. A WebTransport session can carry multiple independent streams and datagrams for its entire lifetime. A useful inspector needs to understand those primitives, reconstruct the streams, and expose their decrypted application bytes.</p>
<p>Chrome&rsquo;s <a href="https://chromedevtools.github.io/devtools-protocol/tot/Network/" rel="external">Network domain</a> emits only <code>webTransportCreated</code>, <code>webTransportConnectionEstablished</code>, and <code>webTransportClosed</code>. These notifications show that a session existed; they do not provide any visibility into what crossed it. There are no corresponding events for streams, datagrams, frames, or payload bytes. Chrome&rsquo;s Network panel provides dedicated message views for <a href="https://developer.chrome.com/docs/devtools/network/reference/" rel="external">WebSockets and event streams</a>, while WebTransport has none. The <a href="https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/" rel="external">Firefox Network Monitor</a> and <a href="https://webkit.org/web-inspector/network-tab/" rel="external">Safari Network tab</a> have the same fundamental limitation: neither provides built-in inspection of WebTransport traffic.</p>
<p>This is not only a concern for native gRPC. Connect and gRPC-Web are more likely starting points for browser RPC work, and they already layer envelopes, Protobuf messages, compression, status, and streaming semantics over HTTP. Moving either protocol onto WebTransport would add session and stream multiplexing beneath those layers while taking away the request and response body visibility developers currently depend on.</p>
<p>Without corresponding browser tooling, diagnosing a single RPC may require correlating application messages, protocol envelopes, WebTransport streams, and server-side transport logs by hand.</p>
<p>When a connection will not establish, Chrome&rsquo;s <a href="https://new.chromium.org/for-testers/providing-network-details/" rel="external"><code>chrome://net-export/</code></a> records browser network events. Wireshark or a QUIC-aware proxy can inspect setup when TLS keys are available. These help with connection failures, but decrypted QUIC still leaves us reconstructing streams and decoding the application protocol.</p>
<p>WebTransport is still in the draft specification phase, so the recommended implementation may change before it is finalized. That is a reason to be cautious about betting a business on it, but not a reason to avoid experimenting with it.</p>
<p>Production applications also need to account for networks that block UDP, which is still common on corporate and managed networks. The specification includes a draft for <a href="https://datatracker.ietf.org/doc/draft-ietf-webtrans-http2/" rel="external">WebTransport over HTTP/2</a>, but browser and server support is much more limited than HTTP/3. It also cannot preserve QUIC&rsquo;s unreliable delivery or stream independence. The Go example in this article only supports HTTP/3, so an application using it needs a fallback to WebSockets, streaming Fetch, or Server-Sent Events.</p>
<h2 id="what-about-grpc">What About gRPC?</h2>
<p>Standard gRPC supports unary, server-streaming, client-streaming, and bidirectional-streaming calls over HTTP/2. Browsers cannot speak native gRPC, so gRPC-Web translates those calls into requests the browser can make. That translation supports unary and server-streaming calls, but it cannot provide client streaming or bidirectional streaming.</p>
<p>The official <a href="https://github.com/grpc/grpc-web/blob/master/doc/streaming-roadmap.md" rel="external">gRPC-Web streaming roadmap</a> has discussed this for years, but the project is no longer pursuing major features. Its issue tracking <a href="https://github.com/grpc/grpc-web/issues/24" rel="external">bidirectional streaming</a> is closed as &ldquo;not planned.&rdquo;</p>
<p>Connect-ES has an open <a href="https://github.com/connectrpc/connect-es/issues/1106" rel="external">WebTransport transport issue</a> motivated by bringing client and bidirectional streaming to browsers. It has no assignee, milestone, or implementation yet, but the interest is there.</p>
<p>WebTransport supplies the bidirectional streams, but not gRPC message framing, metadata, status codes, deadlines, cancellation, or the mapping between RPCs and streams. Those decisions still need an implementation.</p>
<p>The next experiment is to find out how much of gRPC, gRPC-Web, or Connect maps cleanly onto WebTransport without creating an incompatible browser-only protocol.</p>
<h2 id="trying-webtransport-in-go">Trying WebTransport in Go</h2>
<p>To put this all to the test, let&rsquo;s build a simple experiment: a client-server application in Go that uses WebTransport to stream messages. The server will echo whatever the client sends, and the client will send a timestamped message each second. If you&rsquo;ve ever written a small WebSocket app, this will look very familiar.</p>
<h3 id="the-server">The Server</h3>
<p>The server sets up an HTTP/3 server and upgrades incoming requests on the <code>/webtransport</code> endpoint to a WebTransport session. It then enters a loop, accepting new streams and echoing back any messages it receives.</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>cert<span style="color:#eceff4">,</span> err <span style="color:#81a1c1">:=</span> tls<span style="color:#eceff4">.</span><span style="color:#88c0d0">LoadX509KeyPair</span><span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;localhost.pem&#34;</span><span style="color:#eceff4">,</span> <span style="color:#a3be8c">&#34;localhost-key.pem&#34;</span><span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">if</span> err <span style="color:#81a1c1">!=</span> <span style="color:#81a1c1;font-weight:bold">nil</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">return</span> err
</span></span><span style="display:flex;"><span><span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>tlsConfig <span style="color:#81a1c1">:=</span> <span style="color:#81a1c1">&amp;</span>tls<span style="color:#eceff4">.</span>Config<span style="color:#eceff4">{</span>Certificates<span style="color:#eceff4">:</span> <span style="color:#eceff4">[]</span>tls<span style="color:#eceff4">.</span>Certificate<span style="color:#eceff4">{</span>cert<span style="color:#eceff4">}}</span>
</span></span><span style="display:flex;"><span>h3 <span style="color:#81a1c1">:=</span> <span style="color:#81a1c1">&amp;</span>http3<span style="color:#eceff4">.</span>Server<span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	Addr<span style="color:#eceff4">:</span>            <span style="color:#a3be8c">&#34;:4433&#34;</span><span style="color:#eceff4">,</span>
</span></span><span style="display:flex;"><span>	Handler<span style="color:#eceff4">:</span>         mux<span style="color:#eceff4">,</span>
</span></span><span style="display:flex;"><span>	TLSConfig<span style="color:#eceff4">:</span>       tlsConfig<span style="color:#eceff4">,</span>
</span></span><span style="display:flex;"><span>	EnableDatagrams<span style="color:#eceff4">:</span> <span style="color:#81a1c1;font-weight:bold">true</span><span style="color:#eceff4">,</span>
</span></span><span style="display:flex;"><span><span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>server <span style="color:#81a1c1">:=</span> <span style="color:#81a1c1">&amp;</span>webtransport<span style="color:#eceff4">.</span>Server<span style="color:#eceff4">{</span>H3<span style="color:#eceff4">:</span> h3<span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>webtransport<span style="color:#eceff4">.</span><span style="color:#88c0d0">ConfigureHTTP3Server</span><span style="color:#eceff4">(</span>h3<span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mux<span style="color:#eceff4">.</span><span style="color:#88c0d0">HandleFunc</span><span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;/webtransport&#34;</span><span style="color:#eceff4">,</span> <span style="color:#81a1c1;font-weight:bold">func</span><span style="color:#eceff4">(</span>rw http<span style="color:#eceff4">.</span>ResponseWriter<span style="color:#eceff4">,</span> r <span style="color:#81a1c1">*</span>http<span style="color:#eceff4">.</span>Request<span style="color:#eceff4">)</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	session<span style="color:#eceff4">,</span> err <span style="color:#81a1c1">:=</span> server<span style="color:#eceff4">.</span><span style="color:#88c0d0">Upgrade</span><span style="color:#eceff4">(</span>rw<span style="color:#eceff4">,</span> r<span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">if</span> err <span style="color:#81a1c1">!=</span> <span style="color:#81a1c1;font-weight:bold">nil</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>		http<span style="color:#eceff4">.</span><span style="color:#88c0d0">Error</span><span style="color:#eceff4">(</span>rw<span style="color:#eceff4">,</span> err<span style="color:#eceff4">.</span><span style="color:#88c0d0">Error</span><span style="color:#eceff4">(),</span> http<span style="color:#eceff4">.</span>StatusInternalServerError<span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span>		<span style="color:#81a1c1;font-weight:bold">return</span>
</span></span><span style="display:flex;"><span>	<span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">go</span> <span style="color:#81a1c1;font-weight:bold">func</span><span style="color:#eceff4">()</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>		<span style="color:#81a1c1;font-weight:bold">for</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>			stream<span style="color:#eceff4">,</span> err <span style="color:#81a1c1">:=</span> session<span style="color:#eceff4">.</span><span style="color:#88c0d0">AcceptStream</span><span style="color:#eceff4">(</span>context<span style="color:#eceff4">.</span><span style="color:#88c0d0">Background</span><span style="color:#eceff4">())</span>
</span></span><span style="display:flex;"><span>			<span style="color:#81a1c1;font-weight:bold">if</span> err <span style="color:#81a1c1">!=</span> <span style="color:#81a1c1;font-weight:bold">nil</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>				<span style="color:#81a1c1;font-weight:bold">return</span>
</span></span><span style="display:flex;"><span>			<span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>			<span style="color:#81a1c1;font-weight:bold">go</span> io<span style="color:#eceff4">.</span><span style="color:#88c0d0">Copy</span><span style="color:#eceff4">(</span>stream<span style="color:#eceff4">,</span> stream<span style="color:#eceff4">)</span> <span style="color:#616e87;font-style:italic">// Echo this stream independently.</span>
</span></span><span style="display:flex;"><span>		<span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>	<span style="color:#eceff4">}()</span>
</span></span><span style="display:flex;"><span><span style="color:#eceff4">})</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#616e87;font-style:italic">// The complete sample also starts the HTTPS page server and handles shutdown.</span>
</span></span><span style="display:flex;"><span>log<span style="color:#eceff4">.</span><span style="color:#88c0d0">Fatal</span><span style="color:#eceff4">(</span>server<span style="color:#eceff4">.</span><span style="color:#88c0d0">ListenAndServe</span><span style="color:#eceff4">())</span>
</span></span></code></pre></div><p><strong>Note:</strong> This code requires local TLS certificates. I install the amazing <a href="https://github.com/FiloSottile/mkcert" rel="external">mkcert CLI</a> and generate the certificate pair that the sample server loads:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkcert -install
</span></span><span style="display:flex;"><span>mkcert localhost
</span></span></code></pre></div><p>Run these commands from the Go sample directory. <code>mkcert</code> creates <code>localhost.pem</code> and <code>localhost-key.pem</code>, which are loaded by both the Go and Rust servers.</p>
<h3 id="testing-webtransport-in-chrome-and-firefox">Testing WebTransport in Chrome and Firefox</h3>
<p>WebTransport can fail even when HTTPS works. Installing the mkcert CA makes the certificate valid for normal HTTPS, but browsers apply additional restrictions to QUIC and HTTP/3.</p>
<h4 id="firefox">Firefox</h4>
<p>For Firefox, open <code>about:config</code>, set <code>network.http.http3.disable_when_third_party_roots_found</code> to <code>false</code>, and restart Firefox. Firefox otherwise disables HTTP/3 when a user-installed root CA such as mkcert is present. Use a separate development profile because this preference affects the whole profile.</p>
<h4 id="chrome-edge-and-brave">Chrome, Edge, and Brave</h4>
<p>Chrome uses the system trust store, where <code>mkcert -install</code> installs the local CA. WebTransport over HTTP/3 applies an additional requirement that the certificate be issued by a publicly known root. Chromium-based browsers provide a developer mode that relaxes this additional requirement for local testing:</p>
<ol>
<li>Open <code>chrome://flags/#webtransport-developer-mode</code>.</li>
<li>Set <strong>WebTransport Developer Mode</strong> to <strong>Enabled</strong>.</li>
<li>Relaunch the browser.</li>
</ol>
<p>The browser still validates the certificate against the system trust store; this flag allows that trusted root to be a locally installed CA such as mkcert. It is intended only for development. After relaunching, open <code>http://localhost:8080</code>.</p>
<h3 id="the-client">The Client</h3>
<p>The Go client needs only a WebTransport session and a bidirectional stream. The complete sample wraps this in signal handling and separate read/write goroutines, but the transport-specific part is small:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">var</span> dialer webtransport<span style="color:#eceff4">.</span>Dialer
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>_<span style="color:#eceff4">,</span> session<span style="color:#eceff4">,</span> err <span style="color:#81a1c1">:=</span> dialer<span style="color:#eceff4">.</span><span style="color:#88c0d0">Dial</span><span style="color:#eceff4">(</span>
</span></span><span style="display:flex;"><span>	ctx<span style="color:#eceff4">,</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a3be8c">&#34;https://localhost:4433/webtransport&#34;</span><span style="color:#eceff4">,</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">nil</span><span style="color:#eceff4">,</span>
</span></span><span style="display:flex;"><span><span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">if</span> err <span style="color:#81a1c1">!=</span> <span style="color:#81a1c1;font-weight:bold">nil</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">return</span> err
</span></span><span style="display:flex;"><span><span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">defer</span> session<span style="color:#eceff4">.</span><span style="color:#88c0d0">CloseWithError</span><span style="color:#eceff4">(</span><span style="color:#b48ead">0</span><span style="color:#eceff4">,</span> <span style="color:#a3be8c">&#34;done&#34;</span><span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>stream<span style="color:#eceff4">,</span> err <span style="color:#81a1c1">:=</span> session<span style="color:#eceff4">.</span><span style="color:#88c0d0">OpenStreamSync</span><span style="color:#eceff4">(</span>ctx<span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">if</span> err <span style="color:#81a1c1">!=</span> <span style="color:#81a1c1;font-weight:bold">nil</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">return</span> err
</span></span><span style="display:flex;"><span><span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">if</span> _<span style="color:#eceff4">,</span> err <span style="color:#81a1c1">:=</span> stream<span style="color:#eceff4">.</span><span style="color:#88c0d0">Write</span><span style="color:#eceff4">([]</span><span style="color:#81a1c1">byte</span><span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;Hello, WebTransport!&#34;</span><span style="color:#eceff4">));</span> err <span style="color:#81a1c1">!=</span> <span style="color:#81a1c1;font-weight:bold">nil</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">return</span> err
</span></span><span style="display:flex;"><span><span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>reply <span style="color:#81a1c1">:=</span> <span style="color:#81a1c1">make</span><span style="color:#eceff4">([]</span><span style="color:#81a1c1">byte</span><span style="color:#eceff4">,</span> <span style="color:#b48ead">1024</span><span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span>n<span style="color:#eceff4">,</span> err <span style="color:#81a1c1">:=</span> stream<span style="color:#eceff4">.</span><span style="color:#88c0d0">Read</span><span style="color:#eceff4">(</span>reply<span style="color:#eceff4">)</span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">if</span> err <span style="color:#81a1c1">!=</span> <span style="color:#81a1c1;font-weight:bold">nil</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">return</span> err
</span></span><span style="display:flex;"><span><span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>log<span style="color:#eceff4">.</span><span style="color:#88c0d0">Printf</span><span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;Received: %s&#34;</span><span style="color:#eceff4">,</span> reply<span style="color:#eceff4">[:</span>n<span style="color:#eceff4">])</span>
</span></span></code></pre></div><p>The browser side follows the same shape. This example opens three bidirectional streams concurrently and sends one message over each:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">const</span> session <span style="color:#81a1c1">=</span> <span style="color:#81a1c1;font-weight:bold">new</span> WebTransport<span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;https://localhost:4433/webtransport&#34;</span><span style="color:#eceff4">);</span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">await</span> session<span style="color:#eceff4">.</span>ready<span style="color:#eceff4">;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">async</span> <span style="color:#81a1c1;font-weight:bold">function</span> echo<span style="color:#eceff4">(</span>message<span style="color:#eceff4">)</span> <span style="color:#eceff4">{</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">const</span> stream <span style="color:#81a1c1">=</span> <span style="color:#81a1c1;font-weight:bold">await</span> session<span style="color:#eceff4">.</span>createBidirectionalStream<span style="color:#eceff4">();</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">const</span> writer <span style="color:#81a1c1">=</span> stream<span style="color:#eceff4">.</span>writable<span style="color:#eceff4">.</span>getWriter<span style="color:#eceff4">();</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">const</span> reader <span style="color:#81a1c1">=</span> stream<span style="color:#eceff4">.</span>readable<span style="color:#eceff4">.</span>getReader<span style="color:#eceff4">();</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">await</span> writer<span style="color:#eceff4">.</span>write<span style="color:#eceff4">(</span><span style="color:#81a1c1;font-weight:bold">new</span> TextEncoder<span style="color:#eceff4">().</span>encode<span style="color:#eceff4">(</span>message<span style="color:#eceff4">));</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">const</span> <span style="color:#eceff4">{</span> value <span style="color:#eceff4">}</span> <span style="color:#81a1c1">=</span> <span style="color:#81a1c1;font-weight:bold">await</span> reader<span style="color:#eceff4">.</span>read<span style="color:#eceff4">();</span>
</span></span><span style="display:flex;"><span>	<span style="color:#81a1c1;font-weight:bold">return</span> <span style="color:#81a1c1;font-weight:bold">new</span> TextDecoder<span style="color:#eceff4">().</span>decode<span style="color:#eceff4">(</span>value<span style="color:#eceff4">);</span>
</span></span><span style="display:flex;"><span><span style="color:#eceff4">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">const</span> replies <span style="color:#81a1c1">=</span> <span style="color:#81a1c1;font-weight:bold">await</span> <span style="color:#81a1c1">Promise</span><span style="color:#eceff4">.</span>all<span style="color:#eceff4">([</span>
</span></span><span style="display:flex;"><span>	echo<span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;first stream&#34;</span><span style="color:#eceff4">),</span>
</span></span><span style="display:flex;"><span>	echo<span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;second stream&#34;</span><span style="color:#eceff4">),</span>
</span></span><span style="display:flex;"><span>	echo<span style="color:#eceff4">(</span><span style="color:#a3be8c">&#34;third stream&#34;</span><span style="color:#eceff4">),</span>
</span></span><span style="display:flex;"><span><span style="color:#eceff4">]);</span>
</span></span></code></pre></div><p>Each stream is accepted independently. The sample gives every stream its own goroutine, so if work on the first stream stalls, the second and third can continue. This is the part that would require a custom multiplexer over one WebSocket.</p>
<p>The JavaScript version still looks simpler, although removing every <code>if err != nil {</code> line from the Go example does give it a slight head start.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Streaming <code>fetch()</code> and Server-Sent Events fit server-to-client data. WebSockets provide a widely deployed bidirectional channel when one ordered stream is enough. WebTransport fits applications that need independent streams, per-stream backpressure, unreliable datagrams, or isolation from cross-stream head-of-line blocking.</p>
<p>The experiment worked: a browser and a Go client connected to the same Go server over HTTP/3, opened independent bidirectional streams, and exchanged data without routing everything through one ordered WebSocket channel. The programming model is surprisingly simple, and the transport provides primitives that would otherwise require a substantial application-level multiplexer.</p>
<p>Local development also exposed an unexpectedly sharp edge: trusting a mkcert CA for HTTPS does not automatically make every browser accept it for WebTransport over QUIC. Firefox and Chromium both require an explicit development setting. That is fine for an experiment, but it is not something I would want to explain in a production onboarding guide.</p>
<p>WebTransport is the first browser API that exposes modern transport primitives directly enough to solve the independent-stream problem cleanly. Browser support is here. Tooling still lags behind: DevTools cannot inspect the traffic, local certificates need browser-specific setup, and production deployments still need fallbacks.</p>
<p>WebTransport deliberately stops at the transport layer. It does not define an application protocol. A follow-up can take Connect, gRPC-Web, or gRPC framing beyond the transport demo, map an RPC onto WebTransport streams, and test whether the result preserves useful semantics without creating an entirely new protocol in disguise.</p>
]]></content:encoded></item></channel></rss>