This project provides a way to stat a WebSocket connection; measure the latency and learn the transport details. It implements the Go package wsstat and a CLI tool, also named wsstat.
The CLI client provides a simple and easy to use tool to check the status of a WebSocket endpoint:
~ wsstat -v echo.websocket.org
Target: echo.websocket.org
IP: 66.241.124.119
Messages sent: 1
WS version: 13
TLS version: TLS 1.3
DNS Lookup TCP Connection TLS Handshake WS Handshake Message RTT
| 1.541ms | 287.437ms | 321.186ms | 730.487ms | 518.387ms |
| | | | | |
| DNS lookup:1.541ms | | | |
| TCP connected:288.979ms | | |
| TLS done:610.165ms | |
| WS done:1340.652ms |
- Total:2366.209msWithout -v, output is a compact summary:
~ wsstat echo.websocket.org
URL: echo.websocket.org
IP: 66.241.124.119
Round-trip time: 492.884ms (1 message)
Total time: 2296.079msThe client replicates what reorx/httpstat and davecheney/httpstat does for HTTP, but for WebSocket. It is said that imitation is the sincerest form of flattery, and inspiration has for certain been sourced from these projects.
If you are using a Linux distribution that supports Snap, you can install the tool from the Snap Store:
sudo snap install wsstatRequires that you have Go installed on your system and that you have $GOPATH/bin in your PATH. Recommended Go version is 1.26 or later.
Install via Go:
# To install the latest version, specify other releases with @<tag>
go install github.com/jkbrsn/wsstat/v3/cmd/wsstat@latest
# To include the version in the binary, build from a clone of the repo
git clone https://github.com/jkbrsn/wsstat.git
cd wsstat
go install -ldflags "-X main.version=$(cat VERSION)" ./cmd/wsstatNote: a remote go install ...@<tag> cannot inject the version via ldflags, so a binary installed that way reports its version as unknown. Build from a clone (as above) to get the version stamped.
The snap is listed here: snapcraft.io/wsstat
Maintainers: see docs/operations/snap-release-flow.md for how snap revisions are built, published to
edge, and promoted.
Prebuilt release binaries are currently published for Linux/amd64 only. On other platforms, build from source (see macOS and Windows below).
Download the binary from the latest release (amd64) on the release page:
wget https://github.com/jkbrsn/wsstat/releases/download/<tag>/wsstat-<tag>Make the binary executable:
chmod +x wsstatMove the binary to a directory in your PATH:
sudo mv wsstat /usr/local/bin/wsstat # system-wide
mv wsstat ~/bin/wsstat # user-specific, ensure ~/bin is in your PATHCurrently not actively supported, but you may build and try it yourself:
git clone https://github.com/jkbrsn/wsstat.git
cd wsstat
make build-all
# Binary ends up in ./bin/wsstat-<OS>-<ARCH>Then for Windows:
- Place the binary in a directory of your choice and add the directory to your
PATHenvironment variable. - Rename the binary to
wsstat.exefor convenience. - You should now be able to run
wsstatfrom the command prompt or PowerShell.
For macOS:
- Make the binary executable:
chmod +x wsstat-darwin-<ARCH> - Move the binary to a directory in your
PATH:sudo mv wsstat-darwin-<ARCH> /usr/local/bin/wsstat
Some examples:
# Basic request
wsstat wss://echo.example.com
# Send a text message
wsstat -t "ping" wss://echo.example.com
# Read the payload from a file or stdin (@file / @-)
wsstat -t @payload.json wss://echo.example.com
echo '{"hello":"world"}' | wsstat -t @- wss://echo.example.com
# Send an RPC method (JSON-RPC 2.0 by default; --rpc-version 1.0 for legacy servers)
wsstat --rpc-method eth_blockNumber wss://rpc.example.com/ws
# Start a stream
wsstat stream --summary-interval 5s wss://stream.example.com/feed
# Record response payloads to a file as NDJSON (in addition to normal output)
wsstat stream --file capture.ndjson wss://stream.example.com/feed
# Attach headers to dial request
wsstat -H "Authorization: Bearer TOKEN" -H "Origin: https://foo" wss://api.example.com/ws
# Resolve to a target IP and set a longer timeout
wsstat --resolve example.com:443:127.0.0.1 --timeout 30s wss://example.com/ws
# Allow insecure connection, make output extra verbose
wsstat --insecure -vv wss://self-signed.example.comFor a full list of the available options, run wsstat -h, wsstat measure -h, wsstat stream -h, wsstat ping -h, or wsstat check -h.
wsstat <url> (or wsstat measure <url>) measures connection latency. wsstat stream <url> keeps the socket open for long-lived feeds. wsstat ping <url>
watches ping/pong latency over time. wsstat check <url> runs observational RFC
6455 conformance checks. A host literally named
measure/stream/ping/check must be spelled with a scheme
(wsstat wss://ping).
In measure mode, -c N aggregates timing across all interactions; the response
printed is the first one (measure does not concatenate responses).
Long-lived streaming endpoints are exercised with the stream subcommand:
wsstat stream -t '{"method":"subscribe"}' wss://example.org/streamstream keeps the socket open, forwards each incoming frame to stdout, and
periodically snapshots timing metrics. Use -b/--buffer to adjust the
per-subscription queue length and --summary-interval (for example, 30s) to
print recurring summaries that include per-subscription message counts, byte
totals, and mean inter-arrival latency.
measure defaults to -c 1. In stream, -c 0 (the default) keeps the
connection open until you cancel it, while any positive value limits delivery to
that many events before wsstat disconnects:
wsstat stream -c 5 -t '{"method":"subscribe"}' wss://example.org/streamFor a single-response probe, run stream --once, which streams and exits after
the first event:
wsstat stream --once -t '{"method":"subscribe_ticker"}' wss://example.org/wsIn stream, -t may be repeated to hold a multi-frame conversation on a single
connection: each message is sent in argv order, spaced by --send-delay
(default 1s) so the server can answer each frame before the next arrives. The
first message is the subscribe payload; receiving is unchanged, so -c,
--once, and --timeout still bound the read side. If the receive limit is
reached before all messages are sent, the remaining sends are skipped:
wsstat stream -c 4 -o json \
-t '{"method":"subscribe","subscription":{"type":"trades","coin":"BTC"}}' \
-t '{"method":"unsubscribe","subscription":{"type":"trades","coin":"BTC"}}' \
wss://example.org/wswsstat ping <url> dials once and sends a WebSocket ping frame every
-i/--interval (default 1s) on that connection, printing a per-ping RTT line
as each pong arrives and a summary at the end:
wsstat ping -c 5 wss://echo.example.comPING wss://echo.example.com (dns 5ms, tcp 10ms, tls 12ms, ws 7ms)
pong: seq=1 rtt=12.3ms
pong: seq=2 rtt=11.8ms
timeout: seq=3 (5s)
pong: seq=4 rtt=12.1ms
...
STATS wss://echo.example.com (5 sent, 4 received, 20.0% loss)
rtt: min=11.8ms avg=12.1ms max=12.3ms stddev=0.2ms
With no -c the run continues until you interrupt it (Ctrl-C) or the optional
-w/--deadline elapses, like ping(8). -o json emits one ping_reply record
per ping and a final ping_summary (which stays the last record even on total
loss); -q prints the summary block only.
A missed pong (no reply within --timeout, default 5s) is reported as a
timeout line and the run continues, exactly like ping(8) — so a transient
drop shows up as loss in the summary without ending the run. The run ends only at
--count, on Ctrl-C/--deadline, or when the connection actually closes. The
exit code is 0 when at least one pong was received and 1 on total loss or a dial
failure, so wsstat ping -c 3 <url> doubles as a liveness gate.
wsstat check <url> runs a small set of observational RFC 6455 conformance
checks (handshake correctness, subprotocol/extension/version negotiation,
ping/pong, fragmentation tolerance, and close semantics) in a few seconds over at
most 5 connections plus one plain HTTP request, and reports pass/warn/fail/skip
per check in the text or JSON output contract. Every exchange is well-formed: no
malformed frames, no connection storm:
wsstat check wss://echo.example.comHandshake
ok 101 upgrade
ok Sec-WebSocket-Accept valid
ok Upgrade/Connection headers
Negotiation
ok subprotocol (none offered)
ok subprotocol echo
ok permessage-deflate
ok unsupported version rejected
Behavior
ok ping/pong
ok fragmented text
ok close handshake
10 passed, 0 warnings, 0 failed
-v appends the per-check detail and timing; -q prints only the summary line.
-o json emits exactly one check_report record:
wsstat check -o json wss://echo.example.com
# {"schema_version":"1.0","type":"check_report","url":"wss://echo.example.com","checks":[{"id":"handshake.upgrade","group":"handshake","status":"pass","detail":"101 Switching Protocols","took_ms":0.495},{"id":"handshake.accept","group":"handshake","status":"pass","detail":"validated during handshake","took_ms":0},{"id":"handshake.headers","group":"handshake","status":"pass","detail":"Upgrade/Connection tokens present","took_ms":0},{"id":"negotiation.subprotocol-none","group":"negotiation","status":"pass","detail":"none offered, none selected","took_ms":0},{"id":"negotiation.subprotocol-echo","group":"negotiation","status":"pass","detail":"selected wsstat-check","took_ms":0.477},{"id":"negotiation.deflate","group":"negotiation","status":"pass","detail":"permessage-deflate: permessage-deflate","took_ms":0.239},{"id":"negotiation.version-reject","group":"negotiation","status":"pass","detail":"rejected (400, advertises 13)","took_ms":0.235},{"id":"behavior.ping-pong","group":"behavior","status":"pass","detail":"ping -> pong","took_ms":0.084},{"id":"behavior.fragmentation","group":"behavior","status":"pass","detail":"fragmented text accepted","took_ms":0.348},{"id":"behavior.close-echo","group":"behavior","status":"pass","detail":"close 1000 echoed, clean shutdown","took_ms":0.349}],"passed":10,"warned":0,"failed":0,"skipped":0}The exit code is 0 when no check fails (warnings included) and 3 when any check
fails, so wsstat check <url> works as a CI conformance gate. A server that
answers but refuses or botches the upgrade fails the handshake check and skips
the dependents; an unreachable endpoint (DNS, connect, TLS, or timeout — the
endpoint never answered, so nothing was scored), an interrupted run (first
Ctrl-C; the skipped checks left the verdict unanswered), or an output failure
is a runtime error (exit 1).
Skipped by design (blocked by the client always emitting valid, masked frames): unsolicited-pong tolerance, client masking enforcement, and the Tier 2 adversarial probes (invalid UTF-8, RSV bits, reserved/invalid opcodes and close codes, oversized/fragmented control frames), plus limits and performance checks.
Output is split across three orthogonal axes:
-o, --output text|json|raw— the whole-stdout contract.jsonemits newline-delimited envelopes with a stable, published schema (see docs/schema/;-v/-vvnever change which fields appear);rawwrites payload bytes verbatim with nothing added — no label, color, or trailing newline, so frames stay binary-safe and stream frames concatenate undelimited (use-o jsonwhen you need a delimiter). In measure moderawrequires--textor--rpc-method; with--rpc-methodthe response frame is decoded before output, sorawemits compact JSON rather than byte-for-byte wire content.--body auto|compact— human body rendering (text output only).autopretty-prints;compactputs each message on one line.--clip— clips each rendered line to the terminal width with a trailing...(text output, TTY only; a no-op when piped or redirected).
Orthogonal to all three axes, -f/--file <path> additionally records each response
payload to <path> as NDJSON, one per line. It captures response bodies only
(summaries and other chrome still go to stdout), refuses to overwrite an
existing file, and removes the file again if nothing was recorded.
# Machine-readable streaming summaries and events
wsstat stream -o json --summary-interval 5s wss://example.org/stream
# Scannable one-line-per-event output, clipped to the terminal
wsstat stream --body compact --clip wss://example.org/stream--body, --clip, -q, -v, and -vv apply only to -o text and are
rejected under -o json|raw.
HTTP_PROXY, HTTPS_PROXY, and NO_PROXY are honored (loopback and localhost
targets are never proxied). A proxied run reports the proxy and warns that the
per-phase timings no longer describe the target alone:
$ wsstat -v wss://example.org/ws
Proxy: http://proxy.corp:8080
warning: routed through proxy http://proxy.corp:8080; DNS/TCP measure the hop to the proxy; TLS timings do not describe the targetUnder -o json the same information appears as target.proxy plus a warnings
entry. What the TLS numbers mean depends on the proxy's own scheme:
| DNS / TCP | TLS Handshake | Certificates | |
|---|---|---|---|
| direct | target | target | target's |
via http:// proxy |
the proxy hop | not measured (absent) | none reported |
via https:// proxy |
the proxy hop | the proxy connection | the proxy's |
-k/--insecure and custom TLS settings apply to the target handshake in all
three cases. To measure the target itself rather than the route to it, unset the
proxy variables or add the host to NO_PROXY.
| Code | Meaning |
|---|---|
| 0 | Success (also --help and --version) |
| 1 | Runtime failure (dial, measurement, stream, or output write) |
| 2 | Usage error (bad flag or argument) |
| 3 | One or more checks failed (check mode) |
| 130 | Interrupted; a second Ctrl-C forces teardown |
In ping mode exit 1 also covers total loss (zero pongs received), while partial
loss with at least one pong still exits 0, so wsstat ping -c N <url> works as a
liveness gate.
In check mode exit 3 signals that at least one check produced a fail verdict;
warnings still exit 0, so wsstat check <url> works as a CI conformance gate. An
unreachable endpoint or an interrupted run exits 1, never 0: an unanswered
conformance question must not read as a pass.
Usage errors (exit 2) always print plain text to stderr. Under -o json, a
runtime failure (exit 1) prints a {"type":"error"} envelope to stdout so a
wsstat ... -o json | jq pipeline stays parseable on the failure path:
wsstat -o json ws://127.0.0.1:1
# {"schema_version":"1.0","type":"error","error":"measuring latency: ..."}Use the wsstat Golang package to trace WebSocket connection and latency in your Go applications. It wraps coder/websocket for the WebSocket protocol implementation, and measures the duration of the different phases of the connection cycle.
Install to use in your Go project:
go get github.com/jkbrsn/wsstat/v3The examples/main.go program demonstrates two ways to use the wsstat package to trace a WebSocket connection. The example only executes one-hit message reads and writes, but WSStat also support operating on a continuous connection.
Run the example like this, from project root:
go run examples/main.go <a WebSocket URL>The project has a Makefile that provides a number of commands to build and test the project:
# build
make build
make build-all # build for all supported platforms
make clean # remove built binaries and clear the golangci-lint cache
# test
make test
make test V=1 RACE=1 # test with optional flags
# lint
make lintOptional but recommended. Points git at .githooks/, which enforces Conventional Commit subjects (commit-msg), gofmt + lint on new code (pre-commit), and a cached test run (pre-push). Bypass any hook with --no-verify.
make hooksFor contributions, please open a GitHub issue with questions or suggestions. Before submitting an issue, have a look at the existing TODO list to see if what you've got in mind is already in the works.