{{series_nav(current_post=12)}}

HTTP/3 and WebTransport represent the future of web transport protocols, replacing TCP with QUIC (Quick UDP Internet Connections) to eliminate head-of-line blocking, reduce connection establishment latency, and enable new communication patterns. Built on UDP, these protocols offer the reliability of TCP with the flexibility and performance previously only available in custom solutions.

What are HTTP/3 and WebTransport?

HTTP/3 is the third major version of HTTP, using QUIC instead of TCP as its transport protocol. QUIC provides multiplexed streams, built-in encryption, and faster connection establishment.

WebTransport is a new browser API that provides low-level access to QUIC streams and datagrams, enabling bidirectional communication with both reliable streams and unreliable messages.

Core Concepts

QUIC: UDP-based transport protocol with TCP-like reliability plus modern features.

Streams: Independent data flows that don’t block each other (no head-of-line blocking).

Datagrams: Unreliable, unordered messages for low-latency use cases.

0-RTT: Resume connections instantly using previous session data.

Connection Migration: Maintain connections when switching networks (Wi-Fi to cellular).

Encryption by Default: TLS 1.3 integrated into QUIC, cannot be disabled.

Architecture Comparison

graph TB subgraph "Traditional HTTP/1.1 over TCP" H1[HTTP/1.1] T1[TLS 1.2] TCP1[TCP] IP1[IP] H1 --> T1 T1 --> TCP1 TCP1 --> IP1 end subgraph "HTTP/2 over TCP" H2[HTTP/2
Multiplexing] T2[TLS 1.3] TCP2[TCP
Head-of-Line Blocking] IP2[IP] H2 --> T2 T2 --> TCP2 TCP2 --> IP2 end subgraph "HTTP/3 over QUIC" H3[HTTP/3
Multiplexing] Q[QUIC
Streams + TLS 1.3
No HOL Blocking] UDP[UDP] IP3[IP] H3 --> Q Q --> UDP UDP --> IP3 end style TCP2 fill:#ffcccc style Q fill:#ccffcc

Head-of-Line Blocking Elimination

sequenceDiagram participant C as Client participant S as Server rect rgb(255, 230, 230) Note over C,S: HTTP/2 over TCP - HOL Blocking C->>S: Stream 1: Request A C->>S: Stream 2: Request B C->>S: Stream 3: Request C Note over S: Stream 1 packet lost! S--xC: Stream 1: Packet lost Note over C,S: TCP blocks ALL streams
waiting for retransmission S->>C: Stream 1: Retransmitted packet S->>C: Stream 2: Response (delayed) S->>C: Stream 3: Response (delayed) end rect rgb(230, 255, 230) Note over C,S: HTTP/3 over QUIC - No HOL Blocking C->>S: Stream 1: Request A C->>S: Stream 2: Request B C->>S: Stream 3: Request C Note over S: Stream 1 packet lost! S--xC: Stream 1: Packet lost Note over C,S: Only Stream 1 blocked
Streams 2 & 3 proceed independently S->>C: Stream 2: Response (not blocked!) S->>C: Stream 3: Response (not blocked!) S->>C: Stream 1: Retransmitted packet S->>C: Stream 1: Response end

QUIC Connection Establishment

sequenceDiagram participant C as Client participant S as Server rect rgb(255, 240, 240) Note over C,S: TCP + TLS 1.3 (3 RTT) C->>S: SYN S->>C: SYN-ACK C->>S: ACK C->>S: ClientHello (TLS) S->>C: ServerHello + Certificate C->>S: Finished C->>S: HTTP Request Note over C,S: Total: 3 Round-Trip Times end rect rgb(240, 255, 240) Note over C,S: QUIC (1 RTT) C->>S: ClientHello + TLS
(combined handshake) S->>C: ServerHello + Certificate
+ HTTP Response Note over C,S: Total: 1 Round-Trip Time end rect rgb(240, 240, 255) Note over C,S: QUIC 0-RTT (Resumption) C->>S: 0-RTT Data + HTTP Request
(using cached session) S->>C: HTTP Response Note over C,S: Total: 0 Round-Trip Times! end

WebTransport Communication Patterns

graph TB subgraph "Client" C[WebTransport API] end subgraph "QUIC Transport" Q[QUIC Connection] subgraph "Streams" UNI[Unidirectional Streams
Server→Client or Client→Server] BIDI[Bidirectional Streams
Request-Response] end subgraph "Datagrams" DGRAM[Unreliable Datagrams
Fire-and-forget] end end subgraph "Server" S[WebTransport Handler] end C <-->|Reliable
Ordered| BIDI C -->|Reliable
Ordered| UNI S -->|Reliable
Ordered| UNI C <-.->|Unreliable
Unordered| DGRAM S <-.->|Unreliable
Unordered| DGRAM BIDI --> Q UNI --> Q DGRAM --> Q Q --> S

Real-World Use Cases

1. Cloud Gaming & Game Streaming

Ultra-low latency input and video streaming with independent streams.

Perfect for: Stadia, GeForce NOW, Xbox Cloud Gaming, multiplayer games

2. Video Conferencing

Multiple video/audio streams that don’t block each other on packet loss.

Perfect for: Zoom, Google Meet, Teams, WebRTC media servers

3. Live Streaming

Low-latency broadcasting with quick recovery from network issues.

Perfect for: Twitch, YouTube Live, sports broadcasts, live auctions

4. Financial Trading

Real-time market data with datagrams for non-critical updates.

Perfect for: Trading platforms, market data feeds, order books

5. Multiplayer Gaming

Player position updates via datagrams, game state via streams.

Perfect for: FPS games, racing games, MMORPGs, battle royale

6. IoT Sensor Networks

Efficient data transmission from mobile/unstable connections.

Perfect for: Vehicle telemetry, mobile sensors, drone communications

7. CDN & Asset Delivery

Faster page loads with 0-RTT resumption and better multiplexing.

Perfect for: CDNs, image optimization, video delivery, web performance

8. Collaborative Applications

Real-time sync with independent document streams.

Perfect for: Google Docs, Figma, code editors, whiteboarding tools

Project Structure

webtransport-service/
├── cmd/
│   ├── server/
│   │   └── main.go           # WebTransport server
│   └── client/
│       └── main.go           # Go client (testing)
├── internal/
│   ├── server/
│   │   ├── server.go         # WebTransport server
│   │   ├── session.go        # Session management
│   │   └── handler.go        # Message handlers
│   ├── stream/
│   │   ├── stream.go         # Stream handling
│   │   └── datagram.go       # Datagram handling
│   └── protocol/
│       ├── message.go        # Message protocol
│       └── codec.go          # Encoding/decoding
├── pkg/
│   ├── gaming/
│   │   ├── player.go         # Player state
│   │   └── world.go          # Game world
│   └── metrics/
│       └── metrics.go        # Performance metrics
├── web/
│   ├── index.html            # Client UI
│   └── client.js             # JavaScript client
├── certs/
│   ├── cert.pem              # TLS certificate
│   └── key.pem               # TLS private key
├── go.mod
└── go.sum

Implementation

1. Message Protocol

// internal/protocol/message.go
package protocol

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
)

// MessageType defines message types
type MessageType byte

const (
	TypePlayerPosition MessageType = 0x01
	TypeChatMessage    MessageType = 0x02
	TypeGameState      MessageType = 0x03
	TypePing           MessageType = 0x04
	TypePong           MessageType = 0x05
	TypePlayerJoin     MessageType = 0x06
	TypePlayerLeave    MessageType = 0x07
)

// Message represents a protocol message
type Message struct {
	Type      MessageType
	Payload   []byte
	Timestamp int64
}

// PlayerPosition for real-time updates (datagrams)
type PlayerPosition struct {
	PlayerID  string  `json:"player_id"`
	X         float64 `json:"x"`
	Y         float64 `json:"y"`
	Z         float64 `json:"z"`
	Rotation  float64 `json:"rotation"`
	Velocity  float64 `json:"velocity"`
	Timestamp int64   `json:"timestamp"`
}

// ChatMessage for reliable delivery (streams)
type ChatMessage struct {
	PlayerID  string `json:"player_id"`
	Username  string `json:"username"`
	Message   string `json:"message"`
	Timestamp int64  `json:"timestamp"`
}

// GameState for periodic full state sync (streams)
type GameState struct {
	Players   []PlayerPosition `json:"players"`
	Score     map[string]int   `json:"score"`
	Timestamp int64            `json:"timestamp"`
}

// Encode encodes a message to bytes
func (m *Message) Encode() ([]byte, error) {
	// Format: [Type:1][PayloadLen:4][Payload:N]
	payloadLen := len(m.Payload)
	buf := make([]byte, 1+4+payloadLen)

	buf[0] = byte(m.Type)
	binary.BigEndian.PutUint32(buf[1:5], uint32(payloadLen))
	copy(buf[5:], m.Payload)

	return buf, nil
}

// Decode decodes bytes to a message
func Decode(data []byte) (*Message, error) {
	if len(data) < 5 {
		return nil, fmt.Errorf("message too short")
	}

	msgType := MessageType(data[0])
	payloadLen := binary.BigEndian.Uint32(data[1:5])

	if len(data) < int(5+payloadLen) {
		return nil, fmt.Errorf("incomplete message")
	}

	return &Message{
		Type:    msgType,
		Payload: data[5 : 5+payloadLen],
	}, nil
}

// NewPlayerPositionMessage creates player position message
func NewPlayerPositionMessage(pos *PlayerPosition) (*Message, error) {
	payload, err := json.Marshal(pos)
	if err != nil {
		return nil, err
	}

	return &Message{
		Type:      TypePlayerPosition,
		Payload:   payload,
		Timestamp: pos.Timestamp,
	}, nil
}

// ParsePlayerPosition parses player position from message
func ParsePlayerPosition(msg *Message) (*PlayerPosition, error) {
	if msg.Type != TypePlayerPosition {
		return nil, fmt.Errorf("wrong message type")
	}

	var pos PlayerPosition
	if err := json.Unmarshal(msg.Payload, &pos); err != nil {
		return nil, err
	}

	return &pos, nil
}

2. WebTransport Server

// internal/server/server.go
package server

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"sync"
	"time"

	"github.com/quic-go/quic-go"
	"github.com/quic-go/quic-go/http3"
	"github.com/quic-go/webtransport-go"
)

// Server manages WebTransport connections
type Server struct {
	addr     string
	server   *webtransport.Server
	sessions sync.Map // sessionID -> *Session
	tlsConfig *tls.Config

	// Handlers
	onSession      func(*Session)
	onStreamData   func(*Session, []byte)
	onDatagramData func(*Session, []byte)
}

// Config holds server configuration
type Config struct {
	Addr      string
	TLSConfig *tls.Config
}

// NewServer creates a new WebTransport server
func NewServer(cfg *Config) *Server {
	return &Server{
		addr:      cfg.Addr,
		tlsConfig: cfg.TLSConfig,
	}
}

// Start starts the server
func (s *Server) Start(ctx context.Context) error {
	server := &webtransport.Server{
		H3: http3.Server{
			Addr:      s.addr,
			TLSConfig: s.tlsConfig,
			QUICConfig: &quic.Config{
				EnableDatagrams:      true,
				MaxIncomingStreams:   1000,
				MaxIncomingUniStreams: 1000,
				KeepAlivePeriod:      30 * time.Second,
			},
		},
	}

	s.server = server

	// Handle WebTransport sessions
	http.HandleFunc("/webtransport", func(w http.ResponseWriter, r *http.Request) {
		session, err := server.Upgrade(w, r)
		if err != nil {
			log.Printf("Upgrade error: %v", err)
			return
		}

		log.Printf("New session: %s", session.RemoteAddr())
		s.handleSession(ctx, session)
	})

	log.Printf("WebTransport server starting on %s", s.addr)
	return server.ListenAndServe()
}

// handleSession handles a WebTransport session
func (s *Server) handleSession(ctx context.Context, wtSession *webtransport.Session) {
	session := NewSession(wtSession)
	s.sessions.Store(session.ID, session)
	defer s.sessions.Delete(session.ID)

	if s.onSession != nil {
		s.onSession(session)
	}

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	var wg sync.WaitGroup

	// Handle bidirectional streams
	wg.Add(1)
	go func() {
		defer wg.Done()
		s.handleStreams(ctx, session)
	}()

	// Handle datagrams
	wg.Add(1)
	go func() {
		defer wg.Done()
		s.handleDatagrams(ctx, session)
	}()

	// Wait for context cancellation
	<-ctx.Done()
	wg.Wait()

	log.Printf("Session closed: %s", session.ID)
}

// handleStreams handles bidirectional streams
func (s *Server) handleStreams(ctx context.Context, session *Session) {
	for {
		stream, err := session.wt.AcceptStream(ctx)
		if err != nil {
			log.Printf("Accept stream error: %v", err)
			return
		}

		go func(stream webtransport.Stream) {
			defer stream.Close()

			buf := make([]byte, 4096)
			for {
				n, err := stream.Read(buf)
				if err != nil {
					return
				}

				data := make([]byte, n)
				copy(data, buf[:n])

				if s.onStreamData != nil {
					s.onStreamData(session, data)
				}

				// Echo back (or process)
				stream.Write(data)
			}
		}(stream)
	}
}

// handleDatagrams handles unreliable datagrams
func (s *Server) handleDatagrams(ctx context.Context, session *Session) {
	for {
		data, err := session.wt.ReceiveMessage(ctx)
		if err != nil {
			log.Printf("Receive datagram error: %v", err)
			return
		}

		if s.onDatagramData != nil {
			s.onDatagramData(session, data)
		}
	}
}

// Broadcast sends data to all sessions
func (s *Server) Broadcast(data []byte, useDatagram bool) {
	s.sessions.Range(func(key, value interface{}) bool {
		session := value.(*Session)

		if useDatagram {
			session.SendDatagram(data)
		} else {
			session.SendStream(data)
		}

		return true
	})
}

// BroadcastExcept sends data to all sessions except one
func (s *Server) BroadcastExcept(excludeID string, data []byte, useDatagram bool) {
	s.sessions.Range(func(key, value interface{}) bool {
		session := value.(*Session)

		if session.ID != excludeID {
			if useDatagram {
				session.SendDatagram(data)
			} else {
				session.SendStream(data)
			}
		}

		return true
	})
}

// OnSession sets session handler
func (s *Server) OnSession(handler func(*Session)) {
	s.onSession = handler
}

// OnStreamData sets stream data handler
func (s *Server) OnStreamData(handler func(*Session, []byte)) {
	s.onStreamData = handler
}

// OnDatagramData sets datagram data handler
func (s *Server) OnDatagramData(handler func(*Session, []byte)) {
	s.onDatagramData = handler
}

3. Session Management

// internal/server/session.go
package server

import (
	"context"
	"fmt"
	"log"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/quic-go/webtransport-go"
)

// Session represents a WebTransport session
type Session struct {
	ID        string
	wt        *webtransport.Session
	mu        sync.RWMutex
	metadata  map[string]interface{}
	createdAt time.Time
	lastSeen  time.Time
}

// NewSession creates a new session
func NewSession(wt *webtransport.Session) *Session {
	return &Session{
		ID:        uuid.New().String(),
		wt:        wt,
		metadata:  make(map[string]interface{}),
		createdAt: time.Now(),
		lastSeen:  time.Now(),
	}
}

// SendDatagram sends unreliable datagram
func (s *Session) SendDatagram(data []byte) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if err := s.wt.SendMessage(data); err != nil {
		return fmt.Errorf("send datagram: %w", err)
	}

	s.lastSeen = time.Now()
	return nil
}

// SendStream sends reliable stream data
func (s *Session) SendStream(data []byte) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	stream, err := s.wt.OpenStreamSync(context.Background())
	if err != nil {
		return fmt.Errorf("open stream: %w", err)
	}
	defer stream.Close()

	if _, err := stream.Write(data); err != nil {
		return fmt.Errorf("write stream: %w", err)
	}

	s.lastSeen = time.Now()
	return nil
}

// OpenUniStream opens a unidirectional stream
func (s *Session) OpenUniStream(ctx context.Context) (webtransport.SendStream, error) {
	return s.wt.OpenUniStreamSync(ctx)
}

// SetMetadata sets session metadata
func (s *Session) SetMetadata(key string, value interface{}) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.metadata[key] = value
}

// GetMetadata gets session metadata
func (s *Session) GetMetadata(key string) (interface{}, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	val, ok := s.metadata[key]
	return val, ok
}

// Close closes the session
func (s *Session) Close() error {
	return s.wt.CloseWithError(0, "session closed")
}

// RemoteAddr returns remote address
func (s *Session) RemoteAddr() string {
	return s.wt.RemoteAddr().String()
}

4. Game Server Example

// cmd/server/main.go
package main

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"log"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"webtransport-service/internal/protocol"
	"webtransport-service/internal/server"
)

type GameServer struct {
	wt      *server.Server
	players sync.Map // playerID -> *Player
	mu      sync.RWMutex
}

type Player struct {
	ID       string
	Username string
	Position protocol.PlayerPosition
	Session  *server.Session
	LastSeen time.Time
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Load TLS certificates
	cert, err := tls.LoadX509KeyPair("certs/cert.pem", "certs/key.pem")
	if err != nil {
		log.Fatal(err)
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{cert},
		NextProtos:   []string{"h3"},
	}

	// Create game server
	game := &GameServer{}

	// Create WebTransport server
	wtServer := server.NewServer(&server.Config{
		Addr:      ":4433",
		TLSConfig: tlsConfig,
	})

	game.wt = wtServer

	// Set handlers
	wtServer.OnSession(func(session *server.Session) {
		log.Printf("Player connected: %s", session.ID)
		game.handlePlayerJoin(session)
	})

	wtServer.OnDatagramData(func(session *server.Session, data []byte) {
		// Handle player position updates (unreliable, fast)
		game.handleDatagram(session, data)
	})

	wtServer.OnStreamData(func(session *server.Session, data []byte) {
		// Handle chat messages (reliable)
		game.handleStream(session, data)
	})

	// Start game loop
	go game.gameLoop(ctx)

	// Start server
	go func() {
		if err := wtServer.Start(ctx); err != nil {
			log.Fatal(err)
		}
	}()

	log.Println("Game server running on https://localhost:4433")
	log.Println("Connect via: https://localhost:4433/webtransport")

	// Wait for shutdown
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	<-sigChan

	log.Println("Shutting down...")
	cancel()
}

func (g *GameServer) handlePlayerJoin(session *server.Session) {
	player := &Player{
		ID:       session.ID,
		Username: "Player-" + session.ID[:8],
		Session:  session,
		LastSeen: time.Now(),
		Position: protocol.PlayerPosition{
			PlayerID:  session.ID,
			X:         0,
			Y:         0,
			Z:         0,
			Timestamp: time.Now().UnixMilli(),
		},
	}

	g.players.Store(session.ID, player)

	// Send join message to all players
	msg := &protocol.Message{
		Type:    protocol.TypePlayerJoin,
		Payload: []byte(player.Username + " joined"),
	}

	data, _ := msg.Encode()
	g.wt.BroadcastExcept(session.ID, data, false)
}

func (g *GameServer) handleDatagram(session *server.Session, data []byte) {
	msg, err := protocol.Decode(data)
	if err != nil {
		log.Printf("Decode error: %v", err)
		return
	}

	if msg.Type == protocol.TypePlayerPosition {
		pos, err := protocol.ParsePlayerPosition(msg)
		if err != nil {
			return
		}

		// Update player position
		if p, ok := g.players.Load(session.ID); ok {
			player := p.(*Player)
			player.Position = *pos
			player.LastSeen = time.Now()

			// Broadcast position to other players (via datagram for low latency)
			g.wt.BroadcastExcept(session.ID, data, true)
		}
	}
}

func (g *GameServer) handleStream(session *server.Session, data []byte) {
	msg, err := protocol.Decode(data)
	if err != nil {
		log.Printf("Decode error: %v", err)
		return
	}

	if msg.Type == protocol.TypeChatMessage {
		// Broadcast chat to all players (via stream for reliability)
		g.wt.Broadcast(data, false)
	}
}

func (g *GameServer) gameLoop(ctx context.Context) {
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return

		case <-ticker.C:
			// Periodic game state sync
			g.syncGameState()

			// Clean up inactive players
			g.cleanupPlayers()
		}
	}
}

func (g *GameServer) syncGameState() {
	positions := []protocol.PlayerPosition{}

	g.players.Range(func(key, value interface{}) bool {
		player := value.(*Player)
		positions = append(positions, player.Position)
		return true
	})

	state := protocol.GameState{
		Players:   positions,
		Timestamp: time.Now().UnixMilli(),
	}

	payload, _ := json.Marshal(state)
	msg := &protocol.Message{
		Type:    protocol.TypeGameState,
		Payload: payload,
	}

	data, _ := msg.Encode()

	// Send via stream (reliable)
	g.wt.Broadcast(data, false)
}

func (g *GameServer) cleanupPlayers() {
	timeout := 30 * time.Second

	g.players.Range(func(key, value interface{}) bool {
		player := value.(*Player)

		if time.Since(player.LastSeen) > timeout {
			log.Printf("Removing inactive player: %s", player.ID)
			g.players.Delete(key)

			// Notify others
			msg := &protocol.Message{
				Type:    protocol.TypePlayerLeave,
				Payload: []byte(player.Username + " left"),
			}
			data, _ := msg.Encode()
			g.wt.Broadcast(data, false)
		}

		return true
	})
}

5. Web Client (JavaScript)

// web/client.js

class GameClient {
  constructor(url) {
    this.url = url;
    this.transport = null;
    this.playerID = null;
    this.position = { x: 0, y: 0, z: 0 };
    this.otherPlayers = new Map();
  }

  async connect() {
    try {
      // Create WebTransport connection
      this.transport = new WebTransport(this.url);

      await this.transport.ready;
      console.log('WebTransport connected');

      // Start receiving datagrams
      this.receiveDatagrams();

      // Start receiving streams
      this.receiveStreams();

      // Start sending position updates
      this.sendPositionUpdates();

      return true;
    } catch (error) {
      console.error('Connection failed:', error);
      return false;
    }
  }

  async receiveDatagrams() {
    const reader = this.transport.datagrams.readable.getReader();

    while (true) {
      try {
        const { value, done } = await reader.read();
        if (done) break;

        // Decode and handle datagram
        this.handleDatagram(value);
      } catch (error) {
        console.error('Datagram receive error:', error);
        break;
      }
    }
  }

  async receiveStreams() {
    const reader = this.transport.incomingBidirectionalStreams.getReader();

    while (true) {
      try {
        const { value: stream, done } = await reader.read();
        if (done) break;

        // Handle stream data
        this.handleStream(stream);
      } catch (error) {
        console.error('Stream receive error:', error);
        break;
      }
    }
  }

  handleDatagram(data) {
    const message = this.decodeMessage(data);

    if (message.type === 0x01) {
      // Player position update
      const position = JSON.parse(new TextDecoder().decode(message.payload));

      if (position.player_id !== this.playerID) {
        this.otherPlayers.set(position.player_id, position);
        this.updatePlayerPosition(position);
      }
    }
  }

  async handleStream(stream) {
    const reader = stream.readable.getReader();

    while (true) {
      const { value, done } = await reader.read();
      if (done) break;

      const message = this.decodeMessage(value);

      if (message.type === 0x02) {
        // Chat message
        const chat = JSON.parse(new TextDecoder().decode(message.payload));
        this.displayChatMessage(chat);
      } else if (message.type === 0x03) {
        // Game state sync
        const state = JSON.parse(new TextDecoder().decode(message.payload));
        this.syncGameState(state);
      }
    }
  }

  sendPositionUpdates() {
    // Send position updates via datagrams (unreliable, low-latency)
    setInterval(() => {
      const position = {
        player_id: this.playerID,
        x: this.position.x,
        y: this.position.y,
        z: this.position.z,
        rotation: 0,
        velocity: 0,
        timestamp: Date.now(),
      };

      const message = this.encodeMessage(0x01, JSON.stringify(position));

      const writer = this.transport.datagrams.writable.getWriter();
      writer.write(message);
      writer.releaseLock();
    }, 50); // 20 updates per second
  }

  async sendChatMessage(text) {
    const chat = {
      player_id: this.playerID,
      username: 'Player-' + this.playerID,
      message: text,
      timestamp: Date.now(),
    };

    const message = this.encodeMessage(0x02, JSON.stringify(chat));

    // Send via stream (reliable)
    const stream = await this.transport.createBidirectionalStream();
    const writer = stream.writable.getWriter();
    await writer.write(message);
    await writer.close();
  }

  encodeMessage(type, payload) {
    const payloadBytes = new TextEncoder().encode(payload);
    const buffer = new ArrayBuffer(5 + payloadBytes.length);
    const view = new DataView(buffer);

    view.setUint8(0, type);
    view.setUint32(1, payloadBytes.length);
    new Uint8Array(buffer, 5).set(payloadBytes);

    return new Uint8Array(buffer);
  }

  decodeMessage(data) {
    const view = new DataView(data.buffer);
    const type = view.getUint8(0);
    const payloadLen = view.getUint32(1);
    const payload = new Uint8Array(data.buffer, 5, payloadLen);

    return { type, payload };
  }

  updatePlayerPosition(position) {
    // Update player in 3D scene/canvas
    console.log('Player position:', position);
  }

  displayChatMessage(chat) {
    console.log(`${chat.username}: ${chat.message}`);
  }

  syncGameState(state) {
    console.log('Game state synced:', state.players.length, 'players');
  }

  disconnect() {
    this.transport.close();
  }
}

// Usage
const client = new GameClient('https://localhost:4433/webtransport');
await client.connect();

// Send chat
client.sendChatMessage('Hello, world!');

// Update position (continuously via input)
setInterval(() => {
  client.position.x += Math.random() - 0.5;
  client.position.z += Math.random() - 0.5;
}, 100);

Advanced Features

1. Connection Migration

// QUIC automatically handles network changes
// No code needed - works out of the box!

// Client switches from Wi-Fi to cellular -> connection continues
// Server sees same connection ID, no interruption

2. 0-RTT Resumption

// Client configuration for 0-RTT
quicConfig := &quic.Config{
	Allow0RTT: true,
}

// Server configuration
server := &webtransport.Server{
	H3: http3.Server{
		QUICConfig: &quic.Config{
			Allow0RTT: true,
		},
	},
}

// On reconnection, client can send data immediately
// without waiting for handshake

3. Flow Control

// Configure stream-level flow control
quicConfig := &quic.Config{
	InitialStreamReceiveWindow:     512 * 1024,  // 512 KB
	MaxStreamReceiveWindow:         6 * 1024 * 1024, // 6 MB
	InitialConnectionReceiveWindow: 1024 * 1024, // 1 MB
	MaxConnectionReceiveWindow:     15 * 1024 * 1024, // 15 MB
}

4. Priority Streams

// Open high-priority stream
stream, err := session.wt.OpenStreamSync(ctx)
if err != nil {
	return err
}

// Set priority (lower number = higher priority)
// Note: Priority support varies by implementation

Dependencies

// go.mod
module webtransport-service

go 1.21

require (
	github.com/quic-go/quic-go v0.40.1
	github.com/quic-go/webtransport-go v0.6.0
	github.com/google/uuid v1.6.0
)

TLS Certificate Generation

# Generate self-signed certificate for development
openssl req -x509 -newkey rsa:4096 \
  -keyout certs/key.pem \
  -out certs/cert.pem \
  -days 365 -nodes \
  -subj "/CN=localhost"

# For production, use Let's Encrypt or proper CA

Best Practices

1. Choose the Right Transport

Use Datagrams for:

  • Player positions in games
  • Mouse/cursor updates
  • Sensor data
  • Real-time metrics (lossy acceptable)

Use Streams for:

  • Chat messages
  • Game state sync
  • File transfers
  • API requests/responses

2. Error Handling

// Handle connection errors gracefully
func (s *Server) handleSession(ctx context.Context, session *webtransport.Session) {
	defer func() {
		if r := recover(); r != nil {
			log.Printf("Session panic: %v", r)
		}
	}()

	// Monitor connection
	go func() {
		<-session.Context().Done()
		log.Printf("Connection closed: %v", session.Context().Err())
	}()

	// Handle streams/datagrams
}

3. Performance Optimization

// Reuse buffers
var bufferPool = sync.Pool{
	New: func() interface{} {
		return make([]byte, 4096)
	},
}

func handleDatagram(data []byte) {
	buf := bufferPool.Get().([]byte)
	defer bufferPool.Put(buf)

	// Process data
}

// Batch datagram sends
func batchSend(session *Session, messages [][]byte) {
	for _, msg := range messages {
		session.SendDatagram(msg)
	}
}

4. Monitoring

import "github.com/prometheus/client_golang/prometheus"

var (
	datagramsSent = prometheus.NewCounter(prometheus.CounterOpts{
		Name: "webtransport_datagrams_sent_total",
		Help: "Total datagrams sent",
	})

	streamsActive = prometheus.NewGauge(prometheus.GaugeOpts{
		Name: "webtransport_streams_active",
		Help: "Number of active streams",
	})

	latency = prometheus.NewHistogram(prometheus.HistogramOpts{
		Name:    "webtransport_latency_seconds",
		Help:    "WebTransport latency",
		Buckets: prometheus.DefBuckets,
	})
)

When to Use HTTP/3 & WebTransport

Perfect for:

  • Cloud gaming and game streaming
  • Multiplayer games requiring low latency
  • Video conferencing with multiple streams
  • Financial trading platforms
  • Real-time collaborative tools
  • Live streaming with low latency
  • Mobile applications (connection migration)
  • Applications needing both reliable and unreliable delivery

Not ideal for:

  • Legacy browser support required (HTTP/3 support still growing)
  • Simple request-response APIs (use HTTP/2)
  • When UDP is blocked (corporate firewalls)
  • Static file serving (HTTP/2 sufficient)
  • When TLS overhead is concern (QUIC always encrypted)

Conclusion

HTTP/3 and WebTransport represent the cutting edge of web transport technology, solving fundamental limitations of TCP through QUIC’s UDP-based design. By eliminating head-of-line blocking, reducing connection latency, and supporting both reliable streams and unreliable datagrams, these protocols enable new classes of real-time applications that were previously impossible or impractical on the web.

Key takeaways:

  • QUIC eliminates TCP head-of-line blocking via independent streams
  • 0-RTT enables instant reconnection with cached session data
  • Connection migration maintains connections across network changes
  • Use datagrams for low-latency, loss-tolerant data
  • Use streams for reliable, ordered delivery
  • Built-in encryption (TLS 1.3) with no opt-out

HTTP/3 and WebTransport shine when you need ultra-low latency, efficient multiplexing, and mobile-friendly connection handling, making them ideal for the next generation of real-time web applications.


{{series_nav(current_post=12)}}