<dev/>

socket-tester Rewrite: From ExpressJs WebSocket Tool to Multi-Protocol Testing Platform

I rewrote ws-tester from scratch — ExpressJs is gone, replaced by a NestJS proxy backend and a React frontend that supports WebSocket, Socket.IO, MQTT, and AMQP with performance testing, network simulation, and shareable configs.

Apr 18, 2026 ~6 min read
share:
socket-tester Rewrite: From ExpressJs WebSocket Tool to Multi-Protocol Testing Platform

About a year ago I shipped ws-tester — a small ExpressJs app that let you connect to WebSocket and Socket.IO endpoints and fire messages at them. It worked, it was free, and a handful of people used it. But over time, every session exposed the same limitations.

The browser can only reach what the browser is allowed to reach. Custom headers? Mostly blocked. TLS with self-signed certificates? The browser panics. CORS? You’re at the mercy of whatever the target server says. MQTT or AMQP? Not even a conversation.

So I rewrote it entirely. The result is socket-tester — a browser-based testing platform with a NestJS proxy backend and a React frontend. Try it live →

Why a Proxy Backend Changes Everything

The core insight behind the rewrite was simple: move the actual protocol connections out of the browser and into a server process.

Browser (React + Zustand)

    │  HTTP + Socket.IO (control channel)

NestJS Backend
    ├── WebSocket proxy  →  external WS server
    ├── Socket.IO proxy  →  external Socket.IO server
    ├── MQTT client      →  MQTT broker
    └── AMQP client      →  RabbitMQ / AMQP broker

The browser never makes the protocol connection directly. It tells the backend “connect to this endpoint with these parameters,” and the backend manages the actual socket lifecycle. All messages flow back to the browser through a Socket.IO control channel in real time.

This sidesteps every browser limitation at once: custom upgrade headers, TLS certificate trust, CORS, and protocols the browser has no native support for. From the backend’s perspective, it’s just Node.js making network connections — no restrictions.

Four Protocols, One Interface

WebSocket (RFC 6455)

The original use case, now with proper support for what the browser used to block:

  • Custom HTTP upgrade headers
  • Protocol negotiation via Sec-WebSocket-Protocol
  • Manual ping frames with RTT measurement
  • Text, JSON, and binary message types
  • Configurable payload size limit (default 1 MB)

Socket.IO

Full Socket.IO v4 proxy with everything you’d need for a real app:

  • Namespace and path support
  • Auth payload injected at handshake
  • Custom query parameters and transport selection
  • Per-event subscriptions forwarded to the frontend in real time
  • Auto-reconnect with configurable attempts and delay
  • Origin header spoofing for CORS bypass in the proxy context

MQTT

Complete MQTT 3.1.1 client management, including the parts brokers actually use:

  • QoS levels 0, 1, and 2
  • Topic wildcards (+ and #)
  • Last Will and Testament (LWT) message configuration
  • TLS connections with custom CA, client certificate, and key
  • Initial subscriptions set at connection time
  • Buffered message replay for late-joining frontend clients

AMQP / RabbitMQ

AMQP 0.9.1 via amqplib, covering the full declare-bind-consume flow:

  • Exchange types: direct, fanout, topic, headers
  • Queue and exchange declaration with durability and auto-delete flags
  • Queue-to-exchange binding with routing key
  • Consumer with per-delivery ack/nack
  • Publisher to exchange or named queue with content type
  • TLS with custom certificates, heartbeat, and vhost configuration

Performance Testing

One of the features that didn’t exist in any WebSocket tool I’d seen before (for free, in the browser) is built-in performance testing.

Latency test sends a configurable number of ping-pong round trips and collects RTT measurements:

{
  "count": 100,
  "intervalMs": 50
}

Results come back with min, max, avg, p95, and p99 RTTs plus a histogram you can read at a glance.

Throughput test pushes messages at a target rate and measures actual delivery:

{
  "messagesPerSecond": 200,
  "durationSeconds": 10,
  "payloadSize": 512
}

You get sent count, received count, actual rate, and a latency distribution. Tests can be aborted mid-run. Results are archived per test ID and retrievable via REST after the fact — useful when you want to compare runs across configurations.

Network Condition Simulation

This one is useful any time you’re building something that needs to degrade gracefully.

ConditionWhat it does
latencyMsFixed one-way delay on all outbound messages
jitterMsRandom variance on top of the fixed latency
packetLossRateProbability of dropping each message (0.0–1.0)
throughputBytesPerSecBandwidth cap
flappingIntervalMsForces a reconnect every N milliseconds

All of this runs server-side. The target endpoint sees real network degradation — no browser mocking, no fake delays in the frontend. If your app breaks at 20% packet loss, you’ll see it here before you see it in production.

Live Statistics and Logging

Every connection accumulates stats in real time:

  • Sent/received message count and byte totals
  • Latency: last, avg, min, max, p95, p99
  • Error count and reconnect count
  • 60-second rolling time series for charts

The frontend renders these with Recharts: messages per second, latency trends (p95/p99/avg), error rate per minute, and message size histograms. You can watch latency creep up under load without leaving the page.

Logs are structured per connection — up to 5,000 entries with level filtering (info, warn, error, debug) and NDJSON export via GET /api/logs/:connectionId/export.

Shareable Configurations

When you get a connection working exactly right — custom headers, the right auth payload, a specific routing key — you can save that as a shareable token:

POST /api/share/config
→ { "token": "a3f9c1b2d084" }

The token encodes the full configuration. Credentials are stripped before encoding (passwords, API keys, certs don’t travel). Anyone with the token gets a URL that pre-populates the connection form — or a QR code if you’re sharing to a phone.

Tokens expire after 7 days by default, configurable via SHARE_CONFIG_TTL_SECONDS.

Running It Yourself

The repo includes a multi-stage Dockerfile and a docker-compose.yml that starts socket-tester alongside optional Mosquitto and RabbitMQ instances:

git clone https://github.com/abelrgr/socket-tester.git
cd socket-tester
docker compose up --build

That gives you the app on port 3000, an MQTT broker on 1883/9001, and a RabbitMQ management UI on 15672 — everything you need to test any of the four protocols locally without installing anything else.

All configuration is environment-variable driven:

PORT=3000
ALLOWED_ORIGINS=https://yourdomain.com
MAX_CONNECTIONS_PER_IP=10
MAX_MESSAGE_SIZE_BYTES=1048576
SHARE_CONFIG_TTL_SECONDS=604800

The REST API and Swagger

Every action in the UI is backed by a documented REST API. In development mode, Swagger is available at /api/docs. That means you can drive socket-tester programmatically — useful for CI workflows where you want to run a throughput test against a pre-production endpoint and fail the build if p99 latency exceeds a threshold.

Try It

The live version is at websocket-tester.abelgalloruiz.me. Source is on GitHub. Contributions and issues welcome.

If you were using ws-tester before and ran into any of those browser limitations — this is the replacement.