원문 캡처 · blogs.lupyd.com
원문 캡처 · blogs.lupyd.com

웹트랜스포트(WebTransport)는 크롬 97, 파이어폭스 114, 사파리 17.4부터 주요 브라우저에서 지원되지만, 실제 서비스에서는 여전히 웹소켓(WebSocket)이 널리 쓰인다. 루피드(Lupyd)는 브라우저가 아니라 역방향 프록시와 CDN이 서버로 웹트랜스포트 세션을 전달하지 못하는 인프라 문제가 보급을 막고 있다고 설명한다.

  • 웹트랜스포트는 하나의 연결에서 독립적인 스트림과 신뢰성 없는 데이터그램을 제공하지만, 웹소켓 업그레이드의 다른 이름은 아니다.
  • 웹트랜스포트 세션의 데이터는 최초 연결용 CONNECT 스트림에만 흐르지 않는다. 추가 QUIC 스트림과 데이터그램을 별도로 다뤄야 해 기존 프록시가 그대로 중계하기 어렵다.
  • 글은 프로덕션에서 웹트랜스포트를 전달하는 주요 프록시로 Envoy를 꼽는다. 루피드는 자체 메시징 엔진 Firefly에서 웹트랜스포트를 기능 플래그 뒤에 두고 있다.

브라우저 지원은 이미 넓다. 크롬과 엣지 등 크로미움 기반 브라우저는 Chrome 97(2022년 1월)부터 웹트랜스포트를 지원한다. 파이어폭스는 버전 114(2023년 6월)에서 윈도우·macOS·리눅스 데스크톱에 지원을 추가했고, 애플은 Safari 17.4(2024년 3월)부터 iOS·iPadOS·macOS에서 HTTP/3 기반 웹트랜스포트를 기본 활성화했다. Can I Use는 전 세계 활성 이용자의 95% 이상이 브라우저 지원 범위에 든다고 집계한다. W3C 편집자 초안과 MDN API 문서도 공개돼 있다.

그런데 협업 캔버스, 거래 단말, 멀티플레이어 엔진, 메시징 앱 등 실제 서비스의 네트워크 연결을 살펴보면 웹트랜스포트는 드물고, 웹소켓을 TCP나 HTTP/2 위에서 실행하는 경우가 여전히 많다. 루피드는 핵심 병목이 브라우저에서 역방향 프록시와 엣지 라우팅 계층으로 옮겨갔다고 본다. Nginx, HAProxy, Traefik, Cloudflare 같은 제품은 일반 웹 트래픽에 HTTP/3를 지원하지만, 웹트랜스포트 세션을 백엔드로 역방향 프록시하는 데는 제약이 있다고 글은 설명한다.

웹소켓과 다른 전송 방식

웹소켓은 RFC 6455에 정의된 애플리케이션 계층 프레이밍 방식으로, 신뢰성 있고 순서가 보장되는 단일 TCP 바이트 스트림을 사용한다. 텍스트·바이너리 프레임, PING/PONG 하트비트, CLOSE 핸드셰이크를 제공한다. 여러 논리 채널을 하나의 웹소켓에 넣으려면 애플리케이션이 자체 프레이밍과 다중화 헤더, 채널 ID, 흐름 제어를 만들어야 한다.

TCP에서 패킷 하나가 유실되면 재전송될 때까지 뒤따르는 데이터도 지연될 수 있다. 이를 TCP 헤드오브라인(HoL) 차단이라고 한다. 글은 30바이트의 사용자 상태 알림이나 암호화 키 확인 메시지가 500KB 이미지 업로드와 같은 TCP 연결을 공유하면 업로드의 패킷 유실 때문에 지연될 수 있다고 설명한다.

WebRTC 데이터 채널은 UDP와 비슷한 비신뢰·비순서 전송을 제공하지만, 클라우드 서비스와 바이트를 주고받는 데 ICE·STUN·TURN 협상, DTLS 핸드셰이크, 시그널링 서버가 필요해 운영 부담이 크다고 글은 지적한다.

웹트랜스포트는 웹소켓처럼 메시지 프레이밍이나 유형화된 봉투를 제공하는 대신, 임의의 바이트 스트림과 브라우저 자바스크립트에서 직접 보내는 데이터그램 같은 전송 수단을 제공한다. W3C와 IETF가 설계한 추상 전송 계층으로, HTTP/3뿐 아니라 여러 기반 프로토콜 위에서 동작하도록 규정돼 있다.

  • HTTP/3 기반 웹트랜스포트는 주요 브라우저 구현이다. UDP 위의 QUIC(RFC 9000)과 HTTP/3(RFC 9114)를 사용하며, Extended CONNECT와 HTTP 데이터그램(RFC 9297)을 활용한다. 관련 규격은 HTTP/3 기반 웹트랜스포트 초안이다.
  • HTTP/2 기반 웹트랜스포트는 UDP 443 포트를 차단하는 기업 방화벽을 위한 IETF 대체 규격이다. 스트림은 HTTP/2 스트림에 대응시키고, 데이터그램은 TCP 위의 캡슐 프레임에 담는다. 관련 문서는 HTTP/2 기반 웹트랜스포트 초안이다.
  • 원시 QUIC 기반 방식은 HTTP 요청·응답 상태 기계 없이 웹 이외의 환경에 웹트랜스포트 의미 체계를 넣을 수 있다. 관련 문서는 QUIC 기반 웹트랜스포트 초안이다.
  • 추상 API는 레거시 환경에서 기존 양방향 파이프인 웹소켓이나 WebRTC 위에 매핑할 수도 있다. 개요는 IETF 웹트랜스포트 개요 초안에서 확인할 수 있다.

웹트랜스포트는 단일 socket.send(data) 통로 대신 세 가지 전송 수단을 제공한다. 단방향 스트림은 한쪽 방향으로만 흐르는 가볍고 순서가 보장되는 신뢰성 있는 바이트 스트림으로, 파일 조각 업로드나 로그 전송, 서버 브로드캐스트에 쓸 수 있다. 양방향 스트림은 독립적인 요청·응답 흐름을 이루는 두 개의 신뢰성 있는 바이트 스트림이며, 각 스트림이 자체 흐름 제어 한도를 가진다. 한 스트림의 패킷 유실이 다른 스트림의 진행을 막지 않는다.

HTTP 데이터그램은 RFC 9297에 따른 비신뢰·비순서·패킷 크기 단위 데이터다. 패킷이 유실되면 재전송하지 않는다. 원시 UDP와 달리 QUIC 혼잡 제어를 따르므로 링크를 포화시키지 않도록 설계돼 있다. 스트림은 독립적으로 취소할 수도 있다. 한쪽 피어가 stream.writable.abort(errorCode) 또는 stream.readable.cancel()을 호출하면 QUIC의 RESET_STREAM이나 STOP_SENDING 프레임을 보내고, 전체 연결을 끊지 않은 채 양쪽 버퍼 메모리를 해제한다. 반면 웹소켓에서 전송 중인 5MB 메시지를 중단하려면 연결 전체를 닫거나, 쓸모없는 바이트를 계속 읽어 버려야 한다고 글은 설명한다.

글은 웹트랜스포트 API 사용 예로 HTTP/3 연결을 열고 양방향 스트림에서 RPC 요청·응답을 주고받은 뒤, 데이터그램으로 실시간 상태를 전송하는 코드를 제시한다.

typescript
// 1. Establish WebTransport session over HTTP/3 (QUIC)
const transport = new WebTransport("https://gateway.lupyd.com:4433/wt/v1");
// Await the handshake (extended CONNECT with :protocol = webtransport)
await transport.ready;
console.log("WebTransport session established over QUIC connection!");

// 2. Open an independent bidirectional stream for a tiny RPC
async function sendRpc(method: string, payload: Uint8Array): Promise<Uint8Array> {
  const stream = await transport.createBidirectionalStream();
  const writer = stream.writable.getWriter();
    
  // Write request bytes (isolated stream, zero head-of-line blocking)
  await writer.write(payload);
  await writer.close();
  // Read response bytes asynchronously
  const reader = stream.readable.getReader();
  const chunks: Uint8Array[] = [];
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    chunks.push(value);
  }
  return concatBytes(chunks);
}

// 3. Send an unreliable, out-of-order Datagram for real-time presence/typing
const datagramWriter = transport.datagrams.writable.getWriter();
await datagramWriter.write(new TextEncoder().encode("user_42:typing:active"));

프로토콜 비교에서 웹소켓은 TCP와 TLS를 사용하며, 단일 순차 채널에서 TCP의 헤드오브라인 차단을 겪는다. 웹소켓을 HTTP/2 또는 HTTP/3에서 쓰는 표준도 있지만, 웹소켓 자체는 신뢰성 없는 전달을 제공하지 않는다. HTTP/2에서는 TCP 차단이 남고 HTTP/3에서는 연결 단위 차단이 줄어든다. 웹트랜스포트는 QUIC/UDP 위에서 스트림과 데이터그램을 다중화하며, 스트림 간에는 패킷 유실이 전파되지 않는다. 글이 제시한 콜드 핸드셰이크 지연은 웹소켓 3 RTT(TCP SYN, TLS 1.3, HTTP 101), HTTP/2 웹소켓 3 RTT·HTTP/3 웹소켓 1~2 RTT, 웹트랜스포트 1 RTT 또는 0-RTT다. IP가 바뀔 때 웹소켓 연결은 끊기지만 HTTP/3에서는 연결 마이그레이션이 가능하고, QUIC 연결 ID를 쓰는 웹트랜스포트도 이를 기본 지원한다. 웹소켓은 TCP 수신 창을 공유하고 웹트랜스포트는 스트림별 창과 연결 창을 둔다.

역방향 프록시가 막히는 이유

웹소켓 프록시는 비교적 단순한 바이트 중계다. 브라우저가 Upgrade: websocket, Connection: Upgrade 헤더를 넣은 HTTP/1.1 요청을 보내면 프록시는 라우팅 규칙(예: location /ws)에 따라 백엔드로 전달한다. 백엔드가 HTTP/1.1 101 Switching Protocols로 응답하면 프록시는 그 응답을 클라이언트에 보내고, 이후 두 TCP 소켓 사이에서 양방향 바이트를 전달한다. 이 과정에서 프록시는 웹소켓 프레임이나 페이로드를 해석하지 않아도 된다. 기본 관계는 하위 TCP 연결 하나당 상위 TCP 연결 하나다.

웹트랜스포트는 HTTP Upgrade 헤더를 사용하지 않는다. HTTP/3 Extended CONNECT 요청으로 시작한다.

text
// HTTP/3 Extended CONNECT Request Headers:
:method = CONNECT
:protocol = webtransport
:scheme = https
:authority = gateway.lupyd.com
:path = /wt/v1

서버가 해당 HTTP/3 스트림에서 200 OK를 반환하면 그 스트림이 웹트랜스포트 세션이 된다. 하지만 실제 애플리케이션 데이터는 CONNECT 스트림으로 흐르지 않는다. 웹트랜스포트는 기반 QUIC 연결에 추가 스트림과 데이터그램을 만들도록 요청한다.

자바스크립트가 transport.createBidirectionalStream()을 호출하면 CONNECT 스트림 안에 데이터를 잘라 넣는 대신 기존 QUIC 연결에 새 QUIC 스트림을 만든다. HTTP/3는 해당 스트림 앞에 세션 ID를 나타내는 가변 길이 정수(varint)를 붙여 어느 웹트랜스포트 세션에 속하는지 표시한다. transport.datagrams.writable로 보내는 데이터는 QUIC DATAGRAM 프레임 안의 HTTP 데이터그램으로 캡슐화되고, 세션의 4분의 1 스트림 ID가 붙는다. 즉 스트림과 데이터그램은 세션 스트림 안에서 생기는 것이 아니라, 부모 QUIC 연결에서 동적으로 만들어지고 숫자형 세션 ID로 연결된다.

프록시가 클라이언트의 QUIC 연결 전체를 백엔드로 그대로 전달하면 안 되는 이유는 HTTP/3 연결을 여러 웹 요청이 공유하기 때문이다. 같은 연결에는 HTTP/3 제어 스트림, 정적 파일 요청, 인증 API 요청, 웹트랜스포트 CONNECT 세션과 그 하위 스트림이 함께 실릴 수 있다. 프록시가 연결을 통째로 터널링하면 백엔드가 GET /index.html, 정적 이미지, POST /api/v1/auth까지 받게 된다. 엣지 캐시, 웹 애플리케이션 방화벽(WAF), CDN 라우팅, 마이크로서비스별 분리도 무너질 수 있다.

따라서 웹트랜스포트를 중계하는 프록시는 엣지에서 QUIC와 HTTP/3를 종료하고 TLS 1.3, 혼잡 제어, QPACK 압축을 처리해야 한다. Extended CONNECT 요청의 URL 경로·헤더·쿠키를 보고 백엔드를 정한 뒤 세션 ID → 상위 백엔드 인스턴스 매핑을 메모리에 유지해야 한다. 새 QUIC 스트림이 오면 선두 varint를 확인해 세션을 판별하고 해당 백엔드로 전달해야 한다. QUIC 데이터그램도 프레임에서 4분의 1 스트림 ID를 확인해 세션을 찾아 전달해야 한다. 백엔드가 HTTP/3를 지원하지 않는다면 QUIC 스트림과 UDP 데이터그램을 RFC 9298의 CONNECT-UDP나 MASQUE 캡슐 같은 방식으로 변환해야 한다.

이 때문에 웹소켓 프록시는 두 파일 디스크립터 사이에서 바이트를 복사하는 정도로 처리할 수 있지만, 웹트랜스포트는 프록시의 연결 관리자, 다중화, 라우팅 테이블, 흐름 제어, UDP 소켓 전달 구조까지 손봐야 한다는 것이 글의 설명이다.

프록시별 지원 현황

프록시·플랫폼HTTP/3 지원웹트랜스포트 프록시기술 현황과 제약
Nginx예(ngx_http_v3_module)아니요:protocol = webtransport를 거부하며 HTTP 데이터그램(RFC 9297)을 전달하거나 하위 스트림을 백엔드에 연결할 수 없다.
HAProxy예(v2.6 이상, v3.x)아니요HTTP/3 요청·응답을 종료하고 HTTP/2 웹소켓(RFC 8441)은 지원하지만 웹트랜스포트 세션과 다중 스트림 라우팅은 지원하지 않는다.
Traefik예(Traefik v3)아니요요청·응답 중심 라우팅 구조로 HTTP/3 Extended CONNECT의 다중 스트림을 백엔드와 연결하지 못한다.
Cloudflare CDN(Orange Cloud)예(전역 HTTP/3 엣지)아니요(오리진 방향)엣지에서 HTTP/3를 종료하지만 표준 CDN 역방향 프록시는 웹트랜스포트 핸드셰이크를 오리진으로 전달하지 않는다. DNS 전용인 gray cloud 설정이 필요하다.
AWS ALB / CloudFrontCloudFront 엣지만 지원아니요CloudFront는 엣지에서 HTTP/3를 종료한 뒤 오리진에는 HTTP/1.1 또는 HTTP/2로 연결한다. ALB는 백엔드 HTTP/3를 지원하지 않는다.
Caddy예(quic-go 기반)실험적·사용자 설정 필요quic-go/webtransport-go가 있지만 기본 reverse_proxy는 별도 설정 없이 웹트랜스포트를 전달하지 못한다.
Envoy Proxy예(프로덕션 지원)예(완전 지원)하위·상위 HTTP/3, Extended CONNECT, HTTP 데이터그램, 캡슐 캡슐화를 지원한다.
Cloudflare Workers(workerd)예아니요인바운드 웹트랜스포트 API가 구현되지 않았으며 workerd 이슈 #6451에서 요청이 추적되고 있다.

글은 Nginx가 공식 HTTP/3 모듈을 제공한다고 해서 proxy_pass만 설정하면 웹트랜스포트를 전달할 수 있는 것은 아니라고 설명한다. 제시된 설정 예는 핸드셰이크가 거부되는 상황을 보여준다.

nginx
# Attempting WebTransport in Nginx (nginx.conf)
server {
    listen 4433 quic reuseport; # HTTP/3 is active!
    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;

    location /wt {
        # FATAL: Nginx rejects Extended CONNECT requests (:protocol = webtransport)
        # Nginx does not advertise SETTINGS_ENABLE_CONNECT_PROTOCOL or SETTINGS_H3_DATAGRAM.
        # It has no mechanism to demultiplex secondary QUIC streams or forward datagrams
        # to the upstream proxy_pass target.
        proxy_pass https://firefly_upstream;
        proxy_http_version 3; # Not supported upstream in standard Nginx!
    }
}

Nginx는 Extended CONNECT 요청을 405 Method Not Allowed로 거부하거나 :protocol 의사 헤더를 무시한다고 글은 적었다. 표준 Nginx는 백엔드로 HTTP/3를 보내지 못하므로, 초기 핸드셰이크를 받더라도 QUIC 하위 스트림과 HTTP 데이터그램을 전달할 방법이 없다는 설명이다.

글은 현재 프로덕션에서 HTTP/3 웹트랜스포트를 라우팅할 수 있는 주요 역방향 프록시로 Envoy를 꼽는다. Envoy의 HTTP Connection Manager(HCM)는 MASQUE와 Extended CONNECT 처리를 지원하도록 확장됐다. 다음은 글에 실린 설정 예다.

yaml
# Envoy Proxy configuration for WebTransport over HTTP/3
static_resources:
  listeners:
  - name: udp_listener_4433
    address:
      socket_address:
        protocol: UDP
        address: 0.0.0.0
        port_value: 4433
    filter_chains:
    - transport_socket:
        name: envoy.transport_sockets.quic
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.quic.v3.QuicDownstreamTransport
          downstream_tls_context:
            common_tls_context:
              alpn_protocols: ["h3"]
              tls_certificates:
              - certificate_chain: { filename: "/etc/envoy/certs/cert.pem" }
                private_key: { filename: "/etc/envoy/certs/key.pem" }
      filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http3
          codec_type: HTTP3
          http3_protocol_options:
            allow_extended_connect: true # Enables :protocol = webtransport
          http_protocol_options:
            enable_trailers: true
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains: ["*"]
              routes:
              - match:
                  prefix: "/wt"
                route:
                  cluster: firefly_webtransport_backend
                  upgrade_configs:
                  - upgrade_type: "webtransport"
                    connect_config: {}
  clusters:
  - name: firefly_webtransport_backend
    connect_timeout: 0.25s
    type: STRICT_DNS
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http3_protocol_options:
            allow_extended_connect: true

이 설정에는 Envoy의 QUIC 전송 소켓을 사용하는 전용 UDP 리스너, codec_type: HTTP3와 allow_extended_connect: true, 그리고 Extended CONNECT가 활성화된 HTTP/3 백엔드 클러스터가 필요하다. 글은 백엔드 구현에 Rust의 wtransport(quinn 기반), Cloudflare의 quiche, AWS의 s2n-quic을 예로 든다. Envoy의 QUIC 핵심 구현에는 Google quiche를 언급한다.

Cloudflare를 기본 역방향 프록시와 보안 경계로 쓰는 개발자에게도 제약이 있다. Cloudflare는 HTTP/3 도입 초기부터 참여했지만 표준 역방향 프록시인 오렌지 구름 설정은 웹트랜스포트 세션을 오리진으로 전달하지 않는다. 오리진에서 웹트랜스포트를 받으려면 DNS 레코드를 DNS 전용(gray cloud)으로 바꿔 UDP 패킷을 오리진 IP로 직접 보내야 한다. 대신 DDoS 완화, WAF, 봇 관리, 엣지 SSL 종료를 포기하게 된다. 글은 민감한 데이터를 처리하거나 제로 트러스트 요건을 지키는 기업에는 오리진 IP를 UDP 공격에 직접 노출하는 것이 받아들이기 어려운 절충이라고 지적한다.

엣지와 오리진 연결을 줄일 가능성

프록시 지원이 해결되면 웹트랜스포트는 엣지 프록시와 오리진 사이의 연결을 크게 집약할 수 있다는 것이 글의 주장이다. 웹소켓 구조에서는 클라이언트 1만 명이 연결되면 엣지와 오리진 사이에도 개별 상위 TCP 연결 1만 개가 필요하다고 설명한다. 글의 예시에서 오리진은 파일 디스크립터 1만 개와 TCP 커널 버퍼 쌍을 사용하고, 커널 메모리는 약 600MB~1.2GB에 이를 수 있다. 프록시의 임시 포트 고갈과 재접속 시 핸드셰이크 급증 위험도 든다.

웹트랜스포트 구조에서는 엣지 프록시가 클라이언트 QUIC 연결을 각각 종료하고, 세션을 오리진 인스턴스당 하나의 지속적인 상위 HTTP/3 QUIC 연결로 다중화할 수 있다고 글은 설명한다. 이 예시에서는 오리진이 파일 디스크립터 1개와 UDP 소켓 1개를 사용하고, 세션은 인밴드 세션 ID로 구분되며, 커널 버퍼 오버헤드는 5MB 미만이다. 엣지와 오리진 사이의 임시 포트 고갈이 없어지고 새 클라이언트가 연결될 때마다 연결 핸드셰이크를 하지 않아도 된다는 주장이다.

글은 활성 사용자 10만 명이라면 웹소켓은 엣지와 오리진에 각각 10만 개의 소켓이 필요하지만, 웹트랜스포트에서는 프록시가 수천 개의 클라이언트 QUIC 연결을 오리진의 단일 지속 HTTP/3 연결로 합칠 수 있다고 설명한다. 새 클라이언트가 접속하면 프록시는 기존 상위 연결에 :protocol = webtransport인 CONNECT 요청을 보낸다. 클라이언트의 양방향 스트림은 같은 상위 연결의 하위 스트림으로 전달하고, 세션 ID로 구분한다. 비신뢰 데이터그램도 같은 UDP 4-튜플을 통해 전달한다. 글은 이를 통해 오리진의 소켓 고갈, 메모리 부담, 재접속 폭주를 줄일 수 있다고 주장한다. FAQ에서는 오리진 파일 디스크립터와 커널 버퍼 오버헤드를 98% 넘게 줄인다고도 적었다.

Firefly의 구현과 배포 조건

Lupyd는 오픈소스 제로 지식 메시징 엔진 Firefly를 Rust로 개발한다(저장소, Firefly 소개). 데스크톱·모바일·웹 클라이언트는 현재 직렬화한 Protocol Buffers를 담은 지속적인 웹소켓 연결로 서버에 접속한다. 한 연결에서 수십 개의 짧은 요청·응답 RPC와 실시간 메시지 흐름을 함께 처리한다. 예로 신규 연락처의 암호화 사전 키 묶음 요청, MLS(Messaging Layer Security) epoch 커밋 전송, 전달·읽음 확인 응답, 하트비트와 상태 갱신, 암호화된 채팅 메시지, 키 패키지와 ratchet 갱신, 이미지 미리보기, 음성 메시지 조각이 제시됐다.

루피드는 웹소켓의 단일 TCP 연결에서 음성 메시지를 보내는 동안 긴급한 키 갱신 메시지를 받는 상황을 예로 든다. 사용자가 엘리베이터에 들어가거나 기지국을 바꾸면서 패킷이 유실되면, 200KB 암호화 음성 조각의 재전송과 확인이 끝날 때까지 40바이트 MLS 키 회전 커밋도 애플리케이션에 전달되지 않을 수 있다는 설명이다.

Firefly에는 컴파일 시 --features webtransport를 지정하는 기능 플래그 뒤에 웹트랜스포트 클라이언트·서버 드라이버를 구현했다. 기본은 웹소켓이며, 플래그를 켜면 별도 웹트랜스포트 세션을 선택할 수 있다. 글에 실린 드라이버 코드 예시는 다음과 같다.

rust
// In Lupyd's Firefly Engine (firefly-client/src/transport/mod.rs)
// We maintain dual-transport drivers: WebSocket (default) & WebTransport (feature flag)
pub enum TransportDriver {
    WebSocket(WebSocketSession),
    #[cfg(feature = "webtransport")]
    WebTransport(WebTransportSession),
}

impl TransportDriver {
    /// Dispatches a high-frequency cryptographic RPC (e.g. key bundle fetch or delivery ACK)
    pub async fn send_rpc(&self, rpc_id: u64, payload: &[u8]) -> Result<Vec<u8>, TransportError> {
        match self {
            Self::WebSocket(ws) => {
                // Must serialize over a single TCP byte stream with sequence IDs.
                // If a packet drops on bad Wi-Fi, ALL pending RPCs stall behind it!
                ws.dispatch_multiplexed_frame(rpc_id, payload).await
            }
            #[cfg(feature = "webtransport")]
            Self::WebTransport(wt) => {
                // Spawns a dedicated bidirectional QUIC stream for this specific RPC.
                // Zero head-of-line blocking: Packet loss on this stream does not delay other RPCs.
                let mut stream = wt.open_bi().await?;
                stream.write_all(payload).await?;
                stream.finish().await?;
                let response = stream.read_to_end().await?;
                Ok(response)
            }
        }
    }

    /// Emits an ephemeral typing or presence indicator
    pub async fn send_presence(&self, indicator: &[u8]) -> Result<(), TransportError> {
        match self {
            Self::WebSocket(ws) => {
                // Forced to send reliably over TCP, wasting buffers and bandwidth on stale state
                ws.send_text(indicator).await
            }
            #[cfg(feature = "webtransport")]
            Self::WebTransport(wt) => {
                // Fires an unreliable, congestion-controlled HTTP Datagram
                wt.send_datagram(indicator).await
            }
        }
    }
}

루피드는 각 RPC를 wt.open_bi()로 만든 독립 QUIC 스트림에서 처리하면 해당 스트림의 패킷 유실이 다른 RPC를 지연시키지 않는다고 설명한다. 입력 중 표시나 커서 위치 같은 일시적 상태는 wt.send_datagram()으로 보낼 수 있다. 표시가 유실돼도 다음 상태가 200ms 뒤에 도착하면 충분하므로 재전송하지 않아도 되고, 오래된 상태를 보내느라 TCP 버퍼와 대역폭을 쓰지 않는다는 설명이다.

다만 루피드의 로컬 통합 테스트와 내부 컨테이너 벤치마크에서 손실이 있는 네트워크의 지연 시간이 크게 개선됐다는 회사 측 설명만으로는 프로덕션 배포가 이뤄지지 않았다. 회사는 1,500개 엔드포인트 테스트의 데이터와 측정 방법을 별도 글인 손실 네트워크에서의 웹트랜스포트 대 웹소켓 실제 벤치마크에 공개했으며, 해당 글은 셀룰러 패킷 손실에서 꼬리 지연 시간이 80.2% 감소했다고 보고한다. 그러나 루피드는 엣지 프록시와 CDN이 웹트랜스포트 세션을 백엔드 풀로 전달하지 못해 프로덕션 이용자에게 배포할 수 없다고 밝혔다. DDoS 보호 장치와 역방향 프록시 방화벽 없이 데이터베이스와 메시징 오리진을 인터넷에 노출하지 않겠다는 입장이다.

보급을 위해 필요한 변화

글은 웹트랜스포트가 웹의 기본 전송 방식으로 자리 잡으려면 인프라 생태계에서 세 가지 변화가 필요하다고 제안한다. Nginx와 HAProxy가 엣지에서 HTTP/3를 종료하는 데 그치지 않고 Extended CONNECT를 포함한 상위 HTTP/3를 지원해야 한다. 또한 캡슐 라우팅으로 하위 스트림을 애플리케이션 풀에 전달해야 한다. Cloudflare, Fastly, AWS CloudFront 같은 관리형 CDN은 WAF와 DDoS 방어를 유지하면서 웹트랜스포트 트래픽을 오리진으로 터널링해야 한다. 마지막으로 클라이언트 라이브러리는 UDP가 차단되거나 프록시가 핸드셰이크를 거부할 때를 대비해, HTTP/3 웹트랜스포트를 시도하고 엄격한 1초 제한 시간이 지나면 TCP 기반 웹소켓으로 자동 전환하는 이중 전송 절차를 표준화해야 한다.

그때까지 개발자는 Protocol Buffers나 바이너리 프레임처럼 전송 방식에 종속되지 않는 와이어 프로토콜을 설계하고, 메시지 직렬화와 전송 드라이버를 분리하라고 글은 권한다. 프록시 지원이 성숙하면 아키텍처를 다시 작성하는 대신 설정을 바꿔 전송 방식을 전환할 수 있다는 설명이다.