Proxy WebSocket Connections
Create an Apache APISIX Route for a WebSocket echo server and verify a bidirectional message exchange.
Applies to:Apache APISIX 3.18.0
Deployments:DockerRoles:API developerPlatform engineerTime:10 minutesImpact:Changes live traffic
Goal
Start a disposable WebSocket echo server, create one isolated APISIX Route with enable_websocket, and prove that a text message sent through APISIX is returned over the same upgraded connection.
WebSocket begins as an HTTP/1.1 upgrade request, then provides a persistent bidirectional channel over the upgraded TCP connection. APISIX forwards the required upgrade headers only when WebSocket support is enabled on the matched Route or Service.
Before you begin
- Complete Get APISIX on a machine with Docker.
- Install cURL, jq, and websocat. This guide uses websocat's one-message mode for a non-interactive assertion.
Set the endpoints and Admin API key:
export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
export WEBSOCKET_URL="ws://127.0.0.1:9080/websocket-guide/.ws"
read -rsp "Admin API key (press Enter for an unchanged quickstart): " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echoConfirm the quickstart network, Admin API, gateway, and required client are available:
docker network inspect apisix-quickstart-net >/dev/null
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 200
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${GATEWAY_URL}")" != 000
websocat --versionProtect the fixed Route ID, URI, and container name:
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/websocket-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route websocket-guide-route already exists'; exit 1; }
ROUTE_COLLISIONS=$(curl -sS --connect-timeout 3 --max-time 10 "${APISIX_ADMIN_URL}/apisix/admin/routes" -H "X-API-KEY: ${APISIX_ADMIN_KEY}" | jq -r --arg uri '/websocket-guide/.ws' '.list[] | select(.value.uri == $uri or ((.value.uris // []) | index($uri))) | .key')
test -z "${ROUTE_COLLISIONS}" || { printf 'Stop: URI is already used by %s\n' "${ROUTE_COLLISIONS}"; exit 1; }
test -z "$(docker ps -aq --filter name=^/websocket-guide-upstream$)" || { echo 'Stop: container websocket-guide-upstream already exists'; exit 1; }Steps
Step 1: Start the WebSocket echo server
Start the pinned multi-architecture echo-server image on the quickstart network. Host port publication is unnecessary because APISIX reaches the container by its network name:
docker run -d --name websocket-guide-upstream \
--network apisix-quickstart-net \
-e SEND_SERVER_HOSTNAME=false \
jmalloc/echo-server:v0.3.7Wait up to 15 seconds for the upstream TCP listener from the gateway network:
for attempt in {1..15}; do
docker exec apisix-quickstart bash -c '</dev/tcp/websocket-guide-upstream/8080' >/dev/null 2>&1 && break
sleep 1
done
docker exec apisix-quickstart bash -c '</dev/tcp/websocket-guide-upstream/8080'The echo server treats a path whose final component is .ws as a WebSocket endpoint and returns every received message as a WebSocket message.
Step 2: Create the WebSocket Route
Create the Route at its fixed resource URL. The request body is valid JSON and the Admin API key is sent even when the unchanged quickstart accepts an empty value:
curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/routes/websocket-guide-route" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{
"uri": "/websocket-guide/.ws",
"enable_websocket": true,
"upstream": {
"type": "roundrobin",
"nodes": {"websocket-guide-upstream:8080": 1}
}
}'enable_websocket is false by default. When it is true, APISIX forwards the client's Upgrade and Connection headers to the selected upstream instead of treating the request as an ordinary HTTP exchange.
Validate
Send one text message through APISIX, receive one message, and require an exact echo:
WEBSOCKET_RESULT=$(printf 'hello-through-apisix\n' | websocat --one-message --text "${WEBSOCKET_URL}")
test "${WEBSOCKET_RESULT}" = "hello-through-apisix"
printf 'WebSocket echo: %s\n' "${WEBSOCKET_RESULT}"Expected: WebSocket echo: hello-through-apisix. A successful HTTP response from another endpoint would not prove the protocol upgrade or bidirectional message flow; this assertion does both.
Roll back / Clean up
Delete the Route before removing its upstream container, then clear the variables:
curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/websocket-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f websocket-guide-upstream
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_URL WEBSOCKET_URL WEBSOCKET_RESULT ROUTE_COLLISIONSTroubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| websocat receives an HTTP response instead of upgrading. | enable_websocket is absent or false, or another Route matched the URI. | Read back websocket-guide-route, confirm the flag and URI, and inspect the collision check output. |
APISIX returns 502. | The echo container is stopped, not on apisix-quickstart-net, or not listening on port 8080. | Inspect docker logs websocket-guide-upstream, its network attachment, and repeat the bounded TCP check. |
| The client connects but the assertion is empty. | The client exited before receiving a message or connected to the wrong path. | Confirm websocat supports --one-message, use text mode, and keep .ws as the final path component. |
| The command hangs. | A proxy or firewall interferes with the upgraded connection, or the upstream never echoed the message. | Keep the test local, inspect APISIX and upstream logs, and interrupt the client before changing the Route. |