README
¶
HTTP Relay Server
The http-relay-server multiplexes HTTP requests between user-clients and backends (robots) via a relay-client. It exists to make HTTP endpoints on robots accessible without requiring a public endpoint on the robot itself.
How it works
It binds to a public endpoint accessible by both user-client and backend, and works together with a relay-client that's colocated with the backend. This allows multiple backends to be accessible through a single relay-server instance, and supports multiple concurrent user-clients.
flowchart LR
subgraph "LAN (User)"
user-client
end
subgraph Internet
relay-server
end
subgraph "LAN (Robot)"
relay-client
backend
end
user-client -->|HTTP Request| relay-server
%% Workaround https://github.com/mermaid-js/mermaid/issues/3208
relay-server ~~~ relay-client -->|Poll | relay-server
relay-client ~~~ relay-server
relay-client -->|Forward Request| backend
The relay-server is multiplexing: It allows multiple relay-clients to
connect under unique names, each handling requests for a subpath of /client.
Alternatively (e.g. for gRPC connections) the backend can be selected by
omitting the client prefix and passing an X-Server-Name header.
Sequence of operations
- User-client makes request on
/client/$foo/$request. - Relay-server assigns an ID and stores request (with path
$request) in memory. It keeps the user-client's request pending. - Relay-client requests
/server/request?server=$foo - Relay-server responds with stored request (or timeout if no request comes in within the next 30 sec).
- Relay-client makes the stored request to backend.
- Backend replies.
- Relay-client posts backend's reply to
/server/response. - Relay-server responds to client's request with backend's reply.
For some requests (e.g. kubectl exec), the backend responds with
101 Switching Protocols, resulting in the following operations:
- Relay-server responds to client's request with backend's 101 reply.
- User-client sends bytes from stdin to the relay-server.
- Relay-client requests
/server/requeststream?id=$id. - Relay-server responds with stdin bytes from client.
- Relay-client sends stdin bytes to backend.
- Backend sends stdout bytes to relay-client.
- Relay-client posts stdout bytes to
/server/response. - Relay-server sends stdout bytes to the client.
This simplified graphic shows the back-and-forth for an exec request:
sequenceDiagram
participant user-client as user-client<br/>(kubectl exec)
participant relay-server
participant relay-client
participant backend as backend<br/>(k8s apiserver)
user-client->>relay-server: POST /exec
relay-client->>relay-server: GET /request
relay-server-->>relay-client: exec
relay-client->>backend: POST /exec
activate backend
backend-->>relay-client: 101 Switching Protocols
relay-client->>relay-server: POST /response (101)
relay-server-->>user-client: 101 Switching Protocols
user-client->>relay-server: stdin
relay-client->>relay-server: POST /requeststream?id=$id
relay-server-->>relay-client: stdin
relay-client->>backend: stdin
backend-->>relay-client: stdout
deactivate backend
relay-client->>relay-server: POST /response (stdout)
relay-server-->>user-client: stdout
The relay-client side implementation is in ../http-relay-client.
Tested capabilities
The http-relay-server was originally designed as a way to use kubectl against remote clusters. It traverses firewalls by only making outbound requests to the public internet from both the user client (eg kubectl, browser) and the remote cluster. It has been tested with the following traffic:
- HTTP 1.1 & 2 from web browsers (including bidirectional streaming with websockets)
- HTTP 1.1 from kubectl, including streaming response bodies for
kubectl logs - SPDY from kubectl (via HTTP 101 Switching Protocols) for
kubectl exec - Unidirectional gRPC (HTTP2 cleartext and HTTP trailers)
- Streaming gRPC (server, client, and bi-directional)
The following has not been tested:
- HTTP 1.1 streaming request body (
Transfer-Encoding: chunkedin the request header)
Flags
--port: Port number to listen on (default: 80).--block_size: Size of i/o buffer in bytes (default: 10240).--inactive_request_timeout: Timeout for inactive requests (default: 60s). In particular, this sets a limit on how long the backend can wait before writing headers and the response status.
Configuration
Nginx Timeout
If you are running the relay server behind Nginx, ensure that the proxy read timeout on Nginx is set such that Nginx doesn't time out before the http-relay-server does.
Specifically, the nginx.ingress.kubernetes.io/proxy-read-timeout annotation (or proxy_read_timeout directive in nginx config) should be set to a value larger than --inactive_request_timeout.
For example, if --inactive_request_timeout is set to 60s, you might set nginx.ingress.kubernetes.io/proxy-read-timeout to 75s.
Scalability considerations
Single instance capacity and resource usage
A single http-relay-server instance is lightweight and can handle a large number of concurrent connections because it relies on Go's non-blocking I/O and goroutines.
- Idle / long-polling backends: Each connected
http-relay-clientmaintains a long-polling request (/server/request). An idle backend holds only a minimal in-memory state entry (backendState) and one goroutine (~2–8 KB of stack memory). Thus, 1,000 idle backends require ~5–10 MB of RAM, and 10,000 idle backends require less than 100 MB of RAM. - Active requests and streaming: In-flight requests allocate a pendingResponse tracking object and unbuffered channels. Memory consumption for streaming traffic is bounded by the chunk size (the relay client defaults to 1 MB max chunks and 64 KB I/O blocks) and the network buffers for active transfers. For typical workloads with tens to hundreds of concurrent active requests, memory usage remains modest (tens of megabytes).
- Resource recommendation: The default deployment requests
16Mimemory and100mCPU. For production environments handling hundreds of active streams or thousands of connected robots, allocating 512 MB to 1 GB of memory and 1–2 CPU cores provides ample headroom.
System, socket, and infrastructure limits
When scaling to thousands of concurrent connections or high throughput, consider the following non-memory limits:
- File descriptors & sockets: Each open TCP connection (from user clients, long-polling robot clients, or ingress proxies) consumes an OS file descriptor. Ensure the container and host
ulimit -nis configured with sufficient headroom (e.g., 65,535) to preventtoo many open fileserrors. - Kernel TCP socket memory: Outside the Go heap, the Linux kernel allocates read/write buffers (
rmem/wmem) for each open TCP socket. For 10,000+ persistent connections, kernel socket memory can consume several hundred megabytes of system RAM. - Connection churn from long-polling: Relay clients time out and re-poll every 30 seconds. A large fleet (e.g., 10,000 robots) generates a baseline of ~333 requests/sec just for idle polling. Ensure reverse proxies and Ingress controllers allow HTTP keep-alive connections to avoid exhausting ephemeral ports or conntrack table entries (
nf_conntrack_max) with rapid TCP/TLS handshakes. - HTTP/2 stream concurrency: Go's HTTP/2 server (
h2c) defaults to a maximum of 250 concurrent streams per HTTP/2 TCP connection (MaxConcurrentStreams). If a single client or proxy multiplexes more than 250 simultaneous requests over one HTTP/2 connection, additional streams will be queued. - In-memory statefulness: The server stores request-matching state in local memory (broker). A user request and the corresponding robot's long-polling connection must reach the exact same
http-relay-serverprocess. - CPU & GC overhead: Heavy streaming data or high request rates cause CPU load from Protobuf serialization, HTTP/2 framing, and Go garbage collection from frequent byte slice allocations.
Scaling strategies
Because http-relay-server maintains routing state in memory, you cannot simply scale a single deployment horizontally behind a naive round-robin load balancer. To scale beyond a single instance, use sharding:
-
Functional Sharding (by API / Traffic Type):
- Run separate relay server instances for different traffic profiles (e.g., one instance for low-latency Kubernetes API / control-plane RPCs, and another instance for high-throughput streaming such as
kubectl exec, logs, or media streams). - Configure Kubernetes Ingress or Gateway API (HTTPRoute) to route different URL paths or prefixes to the respective relay server service.
- This isolates critical control plane traffic from being starved or delayed by high-bandwidth streaming transfers.
- Run separate relay server instances for different traffic profiles (e.g., one instance for low-latency Kubernetes API / control-plane RPCs, and another instance for high-throughput streaming such as
-
Fleet / Tenant Sharding (by Backend / Robot):
- Partition robots across multiple relay server instances (e.g., shard by robot name, cluster, or tenant).
- Configure each robot's
http-relay-clientwith--relay_prefixor--relay_addresspointing to its designated relay server shard. - Route user traffic to the appropriate shard using distinct hostnames (e.g.,
relay-shard1.example.com) or path prefixes.
Documentation
¶
Overview ¶
Package main runs a multiplexing HTTP relay server.
It exists to make HTTP endpoints on robots accessible without a public endpoint. It binds to a public endpoint accessible by both user-client and backend and works together with a relay-client that's colocated with the backend.
For more details, see README.md.