Development
15 min read
31 views

Multiplexed Multi-Tenant Tunnels: Running 50+ Local Services Over a Single QUIC Connection with HTTP/3 Extended CONNECT

Learn how to run 50+ concurrent local services on a single TCP port using HTTP/3 Extended CONNECT with multiplexed multi-tenant tunnels.

IT
InstaTunnel Team
Published by the InstaTunnel team | Editorial policy
Multiplexed Multi-Tenant Tunnels: Running 50+ Local Services Over a Single QUIC Connection with HTTP/3 Extended CONNECT

Quick answer

Multiplexed Multi-Tenant Tunnels: Run 50+ Services on One: webhook testing answer

For local webhook testing, run your app locally, expose it with a public HTTPS tunnel, and paste the stable callback URL into the provider dashboard.

How do I test webhooks on localhost?

Start your local server, open a public HTTPS tunnel to that port, configure the provider webhook URL, and inspect events in your local logs.

Why does a stable webhook URL matter?

Stable URLs prevent provider dashboards from needing manual callback updates every time you restart a tunnel.

Exposing a handful of local services to the internet is easy. Exposing dozens of them reliably is harder. A typical developer machine or edge gateway may run REST APIs, gRPC endpoints, WebSocket servers and admin UIs at the same time. Giving each one its own port, its own TCP connection, or its own tunnel process gets expensive quickly.

This article looks at how modern tunnels solve the problem. They multiplex many services over one outbound connection, and increasingly that connection is QUIC on a single UDP port (usually 443). It also covers what HTTP/3 Extended CONNECT does and does not give you, because the details matter for tunnel design.


The Bottleneck: One Port or One Connection Per Service

Three problems show up when a tunnel scales with the number of services it exposes.

1. Per-connection overhead

If every exposed service gets its own TCP + TLS connection, the agent pays for it in several ways:

  • Handshake cost. Each connection repeats a TCP handshake and a TLS handshake.
  • Kernel state and file descriptors. Each connection holds a socket, buffers and a TCP state machine. This matters most on small gateways and laptops.
  • NAT and firewall state. Every extra flow is another entry in a stateful device’s table, and NAT mappings eventually time out.

2. Port sprawl

Classic SSH forwarding (-L/-R) needs a distinct listening port for every forwarded service. That means more firewall rules, more port management, and more traffic on unusual ports that restrictive networks often block.

3. TCP head-of-line (HoL) blocking

Multiplexing many logical streams over one TCP connection, as HTTP/2 does, fixes the connection count. It creates a new problem. TCP delivers a single ordered byte stream, so one lost packet stalls every stream behind it until the retransmission arrives. A bulk upload to one service can then delay a latency-sensitive API call sharing the same tunnel.

What existing tunnels already do

Most mature tunnel products already multiplex many services over one connection rather than opening one per service:

  • ngrok uses a stream-multiplexing protocol called muxado, which its open-source Rust and Go SDKs describe as the protocol that powers ngrok’s tunnels.
  • Cloudflare Tunnel (cloudflared) opens four outbound connections to at least two distinct Cloudflare data centers, over port 7844. Its default --protocol auto mode prefers QUIC over UDP and falls back to HTTP/2 when UDP connections can’t be established. The legacy h2mux protocol is no longer supported. cloudflared’s QUIC transport is built on the quic-go library.

The idea of this article, one connection carrying many services, is therefore established practice. The open question is which multiplexing layer to build on and how.


Why QUIC Changes the Picture

QUIC (RFC 9000) runs over UDP and integrates TLS 1.3 (RFC 9001). It has native streams, and loss recovery is tracked per stream. When a packet carrying data for one stream is lost, only that stream waits for retransmission. Other streams on the same connection keep delivering data to the application.

Two caveats keep the usual “zero head-of-line blocking” claim honest:

  • Order is still preserved within a stream. Each stream is its own ordered byte stream, so a loss still delays that stream.
  • HTTP/3 header compression can reintroduce some blocking. QPACK (RFC 9204) was designed to reduce HoL blocking compared to HPACK. References to its dynamic table can still block a stream if the decoder hasn’t yet received the table update. Implementations bound this with the SETTINGS_QPACK_BLOCKED_STREAMS setting.

For tunnels, QUIC adds further benefits:

Property Why it matters for a tunnel
One UDP socket, one connection A single 4-tuple for NATs to track and a single firewall rule (UDP 443).
Per-stream loss recovery A stalled bulk transfer doesn’t freeze unrelated services.
1-RTT handshake, optional 0-RTT resumption Faster reconnects. Be careful with 0-RTT, since early data can be replayed.
Connection IDs and migration A connection can survive a client network change, such as Wi-Fi to cellular, after path validation (RFC 9000).
Built-in DPLPMTUD Packet sizes can grow when the path allows it.

Connection IDs and migration

QUIC connections are identified by Connection IDs, not only by the IP/port 4-tuple. A Connection ID is variable length, up to 20 bytes under RFC 9000. It is not a fixed 64-bit value.

When the agent’s network path changes, the client side can probe the new path and migrate. The endpoints perform path validation first, so migration is not instantaneous or unconditional. Applications and middleboxes can also interfere with it.


HTTP/3 Extended CONNECT: What It Is

Plain HTTP CONNECT opens a TCP tunnel through a proxy. Extended CONNECT was defined for HTTP/2 in RFC 8441 and adds a :protocol pseudo-header, so a single CONNECT stream can carry something other than raw TCP. RFC 9220 brings it to HTTP/3 by registering the SETTINGS_ENABLE_CONNECT_PROTOCOL setting, value 0x08, the same value as in HTTP/2. Despite its title (“Bootstrapping WebSockets with HTTP/3”), the mechanism is general. Other specifications build on it:

  • WebSockets over HTTP/3 (:protocol = websocket), the original use case.
  • CONNECT-UDP (RFC 9298, “Proxying UDP in HTTP”), which tunnels UDP datagrams.
  • CONNECT-IP (RFC 9484, “Proxying IP in HTTP”), which tunnels IP packets.
  • WebTransport over HTTP/3, currently an IETF draft (draft-ietf-webtrans-http3, revision 16 in July 2026), which starts each session with an Extended CONNECT request.

An Extended CONNECT request carries these pseudo-headers:

Pseudo-header Example Notes
:method CONNECT
:protocol websocket, connect-udp, webtransport, or a private token such as tunnel-v1 Names the protocol spoken on the stream. tunnel-v1 is an example for a private design, not a registered value.
:scheme https Required for Extended CONNECT.
:authority service-50.tunnels.example.com Target host, which a tunnel can use for routing.
:path / Required for Extended CONNECT.

Both sides advertise support by sending the setting in their HTTP/3 SETTINGS frame. A client may only send an Extended CONNECT request after the server’s setting has arrived. quic-go, for example, checks this before sending.


The Direction Problem: Who Opens the Stream?

The most important design point, and one easy to get wrong, is who may open streams.

In HTTP/3, request streams are client-initiated bidirectional QUIC streams. The first request goes on stream 0, then 4, 8, 12 and so on. RFC 9114 says HTTP/3 does not use server-initiated bidirectional streams, and a client that receives one must treat it as a connection error of type H3_STREAM_CREATION_ERROR unless an extension has been negotiated.

A tunnel makes this awkward. The agent dials out (it sits behind NAT), so the agent is the QUIC and HTTP/3 client. The edge is the server. But public requests arrive at the edge, and the edge needs to push them toward the agent. In plain HTTP/3 the edge cannot open a request stream to the agent.

Designs that work around this:

  1. Raw QUIC streams with a custom application protocol. QUIC itself lets either endpoint open bidirectional streams. The edge opens one per public request and speaks its own framing on it. This is a general QUIC capability, not HTTP/3 Extended CONNECT. It is the simplest fit for tunnels, and cloudflared builds its QUIC transport on quic-go.
  2. A pool of pre-opened Extended CONNECT streams. The agent opens several idle Extended CONNECT streams to the edge, for example with :protocol = tunnel-v1. The edge parks them. When a public request arrives for a service, the edge claims one parked stream, writes a small preamble naming the target service, and relays bytes. The agent reads the preamble, connects to the local service, copies in both directions, and replenishes the pool. This stays inside standard HTTP/3, at the cost of managing the pool.
  3. WebTransport over HTTP/3. The agent opens one Extended CONNECT request with :protocol = webtransport. Inside that session either endpoint can open streams, because the draft defines this on top of HTTP/3’s unused server-initiated streams. The draft is still an Internet-Draft, so expect changes. WebTransport over HTTP/3 does not support 0-RTT.
  4. Reverse HTTP/3 drafts. Individual drafts describe HTTP/3 with the roles reversed, and “HTTP/3 over WebTransport”, so a server can send requests to a client. These are drafts and not widely deployed.

For a tunnel, option 1 or 2 is the pragmatic choice today. Option 3 is attractive if you want standard browser-compatible plumbing and accept draft status.

Flow with a stream pool (option 2)

   Public client                Edge (QUIC/H3 server)               Agent (QUIC/H3 client)
        |                               |                                   |
        |                               |<== QUIC handshake, UDP 443 =======|
        |                               |<== SETTINGS (ENABLE_CONNECT) =====|
        |                               |<== CONNECT :protocol=tunnel-v1 ===|  stream 0  (parked)
        |                               |<== CONNECT :protocol=tunnel-v1 ===|  stream 4  (parked)
        |  GET https://api.example.com  |                                   |
        |------------------------------>|                                   |
        |                               |-- claim stream 0, send "api" ---->|
        |                               |                                   |-- dial 127.0.0.1:8080
        |<---------- response ----------|<-------- bytes on stream 0 -------|
        |                               |<== agent opens a replacement =====|  stream 8

Each public request maps to one QUIC stream. Fifty services, or fifty concurrent requests, share one UDP socket and one QUIC connection.


What “50+” Means in Practice

Fifty is not a protocol limit. It is a convenient number that happens to sit under a common default. In quic-go, quic.Config.MaxIncomingStreams defaults to 100 concurrent bidirectional streams the peer may open. If you expect more concurrent requests than that, raise it deliberately on the side that receives the streams.

The quic-go documentation lists these flow-control defaults:

Setting Default
InitialStreamReceiveWindow 512 KB
MaxStreamReceiveWindow 6 MB
InitialConnectionReceiveWindow 512 KB
MaxConnectionReceiveWindow 15 MB
MaxIncomingStreams 100

QUIC applies flow control at two levels: per stream and per connection. The stream limit stops one service from consuming all the buffer memory. The connection limit caps the total. The connection window should be sized for the number of streams you expect to be actively transferring at once, not for the sum of every stream’s maximum. If it is too small, streams contend for connection credit and throughput drops.

Tuning the host

QUIC implementations run in user space over UDP, so the kernel’s UDP socket buffers matter. quic-go tries to enlarge its socket buffers but can only go up to the kernel’s rmem_max and wmem_max, which on Linux are small by default. Its documentation recommends:

sysctl -w net.core.rmem_max=7500000
sysctl -w net.core.wmem_max=7500000

That raises the limits to roughly 7.5 MB. Persist them in /etc/sysctl.d/ so they survive a reboot. Cloudflare’s own tooling points users at the same quic-go buffer guidance when it detects small buffers.

Congestion control

Do not assume that switching to BBR is a simple setting. The Linux tcp_congestion_control sysctl affects TCP only, and QUIC’s congestion control lives in the QUIC library. quic-go uses CUBIC. Its maintainers have said they don’t plan to implement BBR, though BBRv3 support is tracked as an open issue. If BBR matters to you, check which congestion controllers your chosen QUIC stack supports.

Throughput versus TCP

QUIC is not always faster. User-space packet processing costs CPU, and mainstream kernels lack some UDP offloads that TCP enjoys. On fast, low-loss wired links, HTTP/3 can be no faster than HTTP/2 and can use more CPU. QUIC helps most on lossy, high-latency or mobile paths. Measure on your own network.


Technical Comparison

Dimension SSH forwarding HTTP/2 over TCP HTTP/3 over QUIC
Transport TCP TCP UDP (QUIC)
Multiplexing Channels inside one TCP connection, or separate connections per forward Streams inside one TCP connection Native QUIC streams
Loss affects All channels on that TCP connection All streams on that TCP connection The affected stream (plus any header-compression dependency)
Handshake TCP + SSH key exchange and authentication TCP + TLS Combined transport and TLS 1.3 handshake, 0-RTT on resumption
Network change Connection breaks Connection breaks Client-initiated migration with path validation
Server-initiated streams Yes (via channels) No (push is deprecated in practice, and Extended CONNECT is client-initiated) Possible in raw QUIC. Not in plain HTTP/3. Possible via WebTransport.
Firewall reach TCP TCP Needs UDP, which some networks block

The Catch: UDP Is Not Always Available

A QUIC tunnel is only useful if UDP gets through. Estimates vary, and the older measurements are dated. Google’s early deployment measurements found roughly 3–5% of networks blocking QUIC, and corporate networks are a known sore spot. Some enterprise firewalls block or rate-limit UDP/443 deliberately so that inspection tools can see traffic on TCP.

Production designs therefore need a TCP fallback. cloudflared’s auto mode does this: it prefers QUIC and falls back to HTTP/2 when UDP connections can’t be established. Your agent should do the same, and it should surface which transport is active so operators can see why performance differs.

Also remember that a UDP flow can still be dropped by NAT timeouts. Send keepalives (QUIC PING frames or an application heartbeat) so idle tunnels don’t silently die.


Security and Multi-Tenant Isolation

Multiplexing many services through one connection means one compromised or misconfigured stream must not reach another tenant’s resources.

  1. Authenticate the connection, not just the streams. Use mutual TLS or a bearer token when the agent first connects. Keep the registration and authentication path out of 0-RTT, because early data can be replayed.
  2. Bind the agent to the hostnames it owns. The edge should only route tenant-a.example.com to the connection that authenticated as tenant A. Never trust a hostname supplied by the agent for routing without checking it against what that tenant is allowed to claim.
  3. Use an allowlist in the agent. The agent should map service names to specific local addresses and reject everything else. In particular, never let a request choose an arbitrary destination address. That blocks server-side request forgery to targets such as the cloud metadata endpoint at 169.254.169.254.
  4. Limit concurrency per tenant. QUIC’s MAX_STREAMS mechanism caps how many streams a peer may open. Add per-tenant rate limits and byte quotas at the edge so a flood on one hostname doesn’t starve the others.
  5. Isolate failures. A reset on one stream should not tear down the connection. Reserve a connection-level close for protocol violations.

Adoption Context

HTTP/3 is no longer exotic. W3Techs reported HTTP/3 in use by about 38.6% of websites in March 2026 and about 39.7% in July 2026. Treat those figures as “sites that advertise support”. Request-level measurements are lower, and Cloudflare Radar data summarized by a third-party analysis put HTTP/3 near 21% of global requests in Q1 2026. For tunnels the most relevant fact is that browsers, CDNs and major QUIC libraries (quic-go, Google’s QUICHE, and others) are mature enough to build on.


Conclusion

Running 50+ local services over one connection is a solved problem. Mature tunnels already multiplex many services over a small number of connections, and QUIC adds per-stream loss recovery, a single UDP port and client-side connection migration. The main points to get right are these:

  • HTTP/3 request streams are client-initiated. A reverse tunnel needs raw QUIC streams, a pool of pre-opened Extended CONNECT streams, or WebTransport, not an edge that simply opens request streams toward the agent.
  • Extended CONNECT (RFC 8441, RFC 9220) is a general mechanism. It carries WebSockets, UDP (RFC 9298), IP (RFC 9484) and WebTransport. Choose or define the :protocol token deliberately.
  • Plan for UDP being blocked. Ship a TCP fallback.
  • Tune and measure. Raise UDP buffer limits, size flow-control windows for expected concurrency, and benchmark against HTTP/2 on your own network before assuming QUIC wins.

Sources

Continue from this article into the most relevant product guides and workflows.

Related Topics

#multiplexed tunnels#multi-tenant tunnels#HTTP/3 Extended CONNECT#HTTP/3 CONNECT method#run multiple services on one port#single TCP port tunneling#cloud ingress stream#microservices tunneling#HTTP/3 multiplexing#local services exposure#reverse proxy tunneling#developer tunneling tools#QUIC protocol tunneling#port multiplexing#bypass port bottlenecks#concurrent local services#HTTP/3 framing#cloud proxy ingress#secure local development#modern tunneling architecture#distributed microservices proxy#scalable tunneling solutions#high-performance tunneling#custom domain tunneling#localhost exposure tool#network address translation traversal#TCP stream multiplexing#zero trust tunneling#edge routing for developers#multi-service port sharing#dynamic service discovery tunnel#low latency tunneling#HTTP/3 transport layer#cloud native networking#containerized service tunneling#enterprise tunneling tools#developer infrastructure#secure ingress proxy#API gateway alternative#local webhooks testing#remote development environments#seamless port forwarding#advanced local tunneling#high concurrency tunnels#HTTP/3 web transport#network optimization tools#cloud tunnel management#developer productivity tools#backend service exposure#scalable cloud ingress#modern network tunneling#enterprise grade tunneling#InstaTunnel features#multi-tenant architecture

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles