Disclosure: I work with 98IP. This post is a vendor-affiliated engineering guide and does not claim that every route or provider behaves identically.
A 101 Switching Protocols response proves that a WebSocket upgrade happened. It does not prove that the connection will carry messages in both directions, survive the real idle timeout, preserve the expected route on reconnect, or fail closed when the proxy disappears.
Treat a proxied WebSocket as a lifecycle with observable checkpoints.
1. Give every run a safe correlation ID
Record only operational metadata:
{
"test_id": "ws-7f2a",
"client": "browser-or-runtime-version",
"proxy_protocol": "http-or-socks5",
"gateway_label": "region-pool-a",
"dns_owner": "gateway",
"address_family": "ipv4",
"expected_route": "proxy-only"
}
Never log proxy passwords, authorization headers, cookies, reusable session tokens, or real message bodies. Use an endpoint you control or are authorized to test.
2. Instrument the whole lifecycle
In browser JavaScript you cannot send protocol-level ping frames directly, so use an application heartbeat when that is the environment under test:
const started = performance.now();
let lastAck = started;
let messagesIn = 0;
let messagesOut = 0;
const ws = new WebSocket(TEST_ENDPOINT, ["test.v1"]);
ws.addEventListener("open", () => {
ws.send(JSON.stringify({ type: "probe", testId: TEST_ID }));
messagesOut += 1;
});
ws.addEventListener("message", (event) => {
messagesIn += 1;
const message = JSON.parse(event.data);
if (message.type === "heartbeat_ack") lastAck = performance.now();
});
const timer = setInterval(() => {
const now = performance.now();
if (now - lastAck > HEARTBEAT_DEADLINE_MS) {
clearInterval(timer);
ws.close(4000, "heartbeat deadline exceeded");
return;
}
ws.send(JSON.stringify({ type: "heartbeat", testId: TEST_ID }));
messagesOut += 1;
}, HEARTBEAT_INTERVAL_MS);
ws.addEventListener("close", (event) => {
console.log({
testId: TEST_ID,
durationMs: Math.round(performance.now() - started),
messagesIn,
messagesOut,
closeCode: event.code,
clean: event.wasClean
});
});
Use performance.now() for durations; it is monotonic. Keep wall-clock timestamps only to correlate client, proxy, and server logs.
3. Prove both directions
An open event is not a bidirectional test.
- Send a unique client probe and require the controlled server to acknowledge it.
- Have the server send a separate message that the client validates.
- Record direction, time, byte count, and a harmless content hash.
Start at concurrency one. If server-to-client traffic fails while client-to-server works, investigate buffering, subscription state, worker lifecycle, and idle controls before rotating the exit.
4. Find the effective idle cutoff
Run the same validated flow with application-idle intervals of 15, 30, 60, 120, and 300 seconds. Probe after each interval. Repeat the first failing boundary several times on the same exit cohort.
Choose a heartbeat interval below the lowest repeatable cutoff with margin for jitter and event-loop delay. Avoid an unnecessarily short interval: at fleet scale, heartbeat traffic becomes real load.
5. Test reconnect policy explicitly
Do not mix these two cases:
- Sticky reconnect: reuse the intended session key and check whether the promised identity scope—IP, region, or ASN—remains stable.
- Fresh reconnect: request a new session and verify that the application recovers correctly on a new exit.
Your assertion must match the product contract. “Same country” is not “same IP.”
Then inject controlled failures: clean server close, abrupt disconnect, proxy refusal, target refusal, DNS failure, and a small concurrent reconnect burst. For every case, define the expected close code, backoff, maximum attempts, session policy, and maximum recovery time.
6. Add a fail-closed route test
Make the configured proxy unavailable. The expected result is a visible failure through the intended route—not a silent direct connection.
Correlate the client test ID with proxy-side evidence. A client-side “connected” state alone cannot prove which path was used.
Minimum release matrix
| Case | Evidence | Pass condition |
|---|---|---|
| Upgrade | status, subprotocol, route ID | valid upgrade through intended gateway |
| Client → server | acknowledgement | unique probe received once |
| Server → client | client validation | independent message received before deadline |
| Idle | stepped intervals | cutoff measured and repeatable |
| Heartbeat | send/ack times | timeout closes or quarantines socket |
| Sticky reconnect | exit-cohort evidence | promised identity scope preserved |
| Fresh reconnect | new-session evidence | recovery works on a new route |
| Proxy unavailable | route observation | no direct fallback |
| IPv4/IPv6 | family-specific run | expected behavior documented |
The result should be a reproducible record, not a screenshot of a green connection badge.
More testing and routing guides from the team I work with: https://en.98ip.com/?k=dev
``
Top comments (0)