Skip to main content

Real-time Debugging

Hooklistener gives you a hosted WebSocket, Socket.IO, MQTT or Server-Sent Events (SSE) endpoint for your client to connect to. Every connection becomes a session, and every message in either direction is recorded. You can answer clients from the console, the REST API, MCP, rules and scenarios. You can also simulate a bad network, or relay each connection to your real server in proxy mode.

Looking for MCP diagnostics?

Testing your own MCP server now has its own section in the app and in these docs. See MCP diagnostics.

How it works​

  1. Open Real-time in the sidebar, pick a protocol tab (WebSocket, Socket.IO, MQTT or SSE) and click Create Endpoint or Quick Create.
  2. Copy the connection URL or a ready-made snippet from the endpoint's Connect tab.
  3. Connect your client. Each connection opens a session in the Console tab.
  4. Inspect what the client sends, answer it, and change the endpoint's behaviour from the Rules, Scenarios, Expectations and Settings tabs.

Protocols​

ProtocolConnect toGuide
WebSocketwss://app.hooklistener.com/ws/<slug>WebSocket
Socket.IOio("https://app.hooklistener.com", { path: "/sio/<slug>" })Socket.IO
MQTT over WebSocketwss://app.hooklistener.com/mqtt/<slug>MQTT
Server-Sent EventsGET https://app.hooklistener.com/sse/<slug> to stream, POST to sendSSE

You choose the protocol when you create the endpoint. The slug can be changed in Settings, which changes the connection URL.

Access token​

Anyone who knows an endpoint's URL can connect to it until you set an access token under Settings → Access control (Generate creates one). Clients can then present it in any of these ways:

WhereExampleUse it for
Query string?token=<token> (or ?access_token=)Any client, including browsers and EventSource
HeaderX-Hooklistener-Token: <token>Clients that can set headers
Bearer headerAuthorization: Bearer <token>Clients that can set headers
WebSocket subprotocolnew WebSocket(url, ["hooklistener-token.<token>"])Browsers, which can't set WebSocket headers. See WebSocket
Socket.IO authio(origin, { path, auth: { token } })Socket.IO v5 clients. See Socket.IO
MQTT CONNECTPassword (any username)MQTT clients. See MQTT

A missing or wrong token is refused before a session opens:

  • On the HTTP handshake: 401 with {"error": "Missing or invalid access token"}.
  • On Socket.IO: a connect_error.
  • On MQTT: CONNACK "not authorized".

The token is never stored with the session's recorded headers or query string, and it is never forwarded to a proxy upstream.

Under Allowed origins you can list the browser origins allowed to connect, one per line. https://*.example.com allows every subdomain. The check only applies to clients that send an Origin header (browsers); others get 403 Origin not allowed. An empty list allows every origin.

Setting an endpoint to Paused refuses new connections with 503 Endpoint is paused. Sessions that are already open stay open.

Keepalive​

Proxies and load balancers cut connections that carry no traffic. Cloudflare, for example, closes them after about 100 seconds. Hooklistener keeps quiet connections alive:

  • WebSocket, Socket.IO and MQTT: the server sends a WebSocket ping every 25 seconds. A client that sends nothing for 75 seconds, not even the pong, is closed with 1001 and the reason keepalive timeout: nothing received from the client for 75s. Browsers and WebSocket libraries answer pings on their own. Keepalive pings and pongs are never recorded.
  • Socket.IO also runs its own Engine.IO heartbeat. See Socket.IO.
  • MQTT also enforces the client's keep alive interval, at 1.5 times its value. See MQTT.
  • SSE: the server writes a : keepalive comment after 30 seconds without other writes. You can set the interval between 1 and 90 seconds. See SSE.

Limits​

LimitValueWhen exceeded
Message size1 MB per messageClosed with 1009 and message too large: <n> bytes (limit 1 MB). An SSE POST gets 413.
Message rate200 messages per second per connection by defaultClosed with 1008 and rate limit exceeded: more than 200 messages/s
Data rate2 MB per second per connectionClosed with 1008 and rate limit exceeded: more than 2 MB/s
Open sessions200 per endpointNew connections get 503 Endpoint has too many active debug sessions
Connection attempts60 per minute per client IP and endpoint, 300 per minute per endpoint, 1,500 per minute per organization429 Too many connection attempts
Stored payloadFirst 128 KB of each messageThe message is delivered in full; the console marks it as truncated
EndpointsSet by your planCreating one more returns 403 Real-time endpoint limit reached for your plan
HistoryYour plan's request historyOlder sessions and their messages are deleted. Pinned sessions are kept.

You can change the message rate under Settings → Access control → Inbound rate limit, up to 10,000. 0 uses the default of 200. The close reason is shown on the session. Messages received before the limit stay recorded.

The console​

Each endpoint has six tabs: Console, Rules, Scenarios, Expectations, Connect and Settings.

In the Console tab:

  • The session list shows every connection, with its client address, close code and reason. When a client reconnects, the console switches to its new session.
  • The message list shows each message's direction, frame type, event name or topic, size and time. It flags dropped, duplicated, truncated and schema-invalid messages. You can search across sessions, or filter by direction, frame type and source.
  • The composer sends to This session or to every live session, as Text or Binary (b64). It shows the fields each protocol needs:
    • the event name;
    • the Socket.IO namespace;
    • the MQTT topic, QoS and retain flag;
    • the SSE id and retry.
  • For each session you can:
    • label, pin or ping it;
    • close it with a code and reason;
    • give it its own network simulation;
    • share it through a public read-only link, valid for 7 days;
    • download it as NDJSON or JSON.

Close codes​

Every closed session records the close code and reason, and why it ended (client_closed, server_closed, rate_limited, keepalive_timeout, simulated_disconnect, upstream_closed and so on). The code and reason come from the client's close frame when the client closed. A connection that dropped without a close frame is recorded as 1006.

CodeMeaning
1000Normal close: by the client, or by you from the console or API (the default code)
1001Keepalive timeout, Close all from the API (its default code), or endpoint deleted
1006The connection dropped without a close frame
1008Rate limit exceeded, or an authentication failure inside the protocol (MQTT CONNECT, Socket.IO auth)
1009Message larger than 1 MB
1011Simulated disconnect (the default code), or the proxy upstream failed

Rules​

Rules answer clients automatically. They run, in order, on every message a client sends, and the first matching rule fires. If that rule's Stop processing option is off, later matching rules fire too.

Match on:

  • Every message
  • Payload contains a string
  • Payload matches regex
  • JSON conditions: the same expression language as mock response rules, evaluated against the decoded payload
  • Event name equals: the Socket.IO event, the SSE event query parameter, or the MQTT topic. MQTT topic filters with + and # work here.

Actions:

  • Reply to the client with a payload (and an event name or topic).
  • Reply as the event's ack (Socket.IO): the payload answers an emitWithAck callback. See Socket.IO acks.
  • Echo the message back: the same bytes and frame type, never templated.
  • Broadcast to every session of the endpoint.
  • Close the connection with a code and reason.

Each rule can wait up to 30 seconds before it acts. Replies go back where the message came from, for example to the same Socket.IO namespace.

Scenarios​

A scenario is a script of steps played into one session or into every live session. Steps are send (a payload, after an optional delay), wait, ping and close. A scenario can have up to 500 steps and can loop. Record from session turns a session's outgoing messages into a scenario that keeps their original timing.

Expectations​

An expectation says that a message must arrive within a time limit after each session connects. For example: "the client sends a message containing subscribe within 5 seconds". You choose:

  • the direction: Received from client or Sent to client;
  • a match, with the same match types as rules;
  • a timeout between 100 ms and 10 minutes.

Each session shows whether every expectation is pending, passed or failed.

Contract validation​

Paste a JSON Schema under Settings → Contract validation and choose whether it applies to messages received from clients, messages sent to clients, or both. Messages that fail are flagged in the console with the validation errors. They are still delivered.

Network simulation​

The console's network simulation panel degrades the connection for every session of the endpoint. You can also give a single session its own override.

SettingRangeEffect
Latency0–30,000 msDelays every message sent to the client
Jitter0–10,000 msAdds random variation to the latency
Drop0–100%Records the message as dropped instead of sending it
Duplicate0–100%Sends the message twice
Inbound delay0–30,000 msDelays processing of each message the client sends
Disconnect0–100%Closes the connection after a client message, with the Disconnect close code (default 1011). On SSE it also fires after events sent to the client.

A simulated disconnect drops the connection the way a network failure would, so clients reconnect. On Socket.IO it never sends the "server disconnect" packet. See Socket.IO reconnection.

Templating​

Payloads can contain {{variables}}:

VariableValue
{{uuid}}A random UUID v4
{{now}}ISO 8601 UTC timestamp
{{timestamp}} / {{unix}}Unix time in milliseconds / seconds
{{seq}}Per-session counter of messages sent: 1, 2, 3…
{{random.int}}, {{random.int:MIN:MAX}}, {{random.float}}, {{random.hex:N}}, {{random.pick:a|b|c}}Random values
{{message}}, {{message.path.to.field}}The message that triggered a rule, or one of its JSON fields
{{event}}The triggering event name
{{session.id}}, {{session.remote_addr}}, {{session.label}}, {{session.client_id}}The session (client_id is the MQTT client id)
{{endpoint.slug}}, {{endpoint.name}}, {{endpoint.id}}The endpoint

Variables are rendered only in rule replies, on-connect payloads and scenario sends. Everything else is sent byte for byte unless you opt in:

  • Console: the composer's {{ }} Variables on / Verbatim toggle. It is on by default and applies to text messages.
  • REST API: add "render_template": true to the send or broadcast body. Without it the payload is sent verbatim.
  • MCP: send_realtime_message renders text payloads.

Echoes and proxied frames are never rendered.

On connect​

Under Settings → On connect you can set a payload that every new client receives. You can add an event name (Socket.IO, SSE) or topic (MQTT), and a delay of up to 30 seconds. Socket.IO sends it to each namespace once that namespace connects. MQTT sends it after CONNACK; see MQTT on-connect.

Proxy mode​

In proxy mode Hooklistener sits between your client and your real server. Under Settings → Proxy mode, turn on Enable proxy and enter the Upstream URL (ws://, wss://, http:// or https://). Every client connection opens its own connection to the upstream, and both directions are recorded.

  • WebSocket, Socket.IO and MQTT frames are relayed byte for byte. Your server, not Hooklistener, answers the protocol handshake. For SSE, the upstream's events are relayed to the client, and messages the client POSTs are forwarded to the upstream, whose answer the client gets.
  • Rules, expectations and contract validation still run on what the client sends. From the console you can inject messages → client or → upstream.
  • The endpoint's access token is never forwarded. Extra upstream headers are added to every upstream request. Forward the client's … headers passes on some of the client's own headers, such as its Authorization.
  • The upstream must be reachable from the internet. Private and internal addresses are refused.

Each protocol page explains what proxy mode does for that protocol.

REST API and MCP​

Everything in the console is also available under /api/v1/realtime, authenticated like the rest of the REST API:

RequestPurpose
GET /api/v1/realtime/endpointsList endpoints with their connection details and live session counts
POST /api/v1/realtime/endpointsCreate an endpoint (name, protocol and any setting)
GET /api/v1/realtime/endpoints/:id/sessionsList sessions
GET /api/v1/realtime/endpoints/:id/sessions/:session_id/messagesRead a session's messages
POST /api/v1/realtime/endpoints/:id/sessions/:session_id/messagesSend to one session
POST /api/v1/realtime/endpoints/:id/broadcastSend to every open session
POST /api/v1/realtime/endpoints/:id/sessions/:session_id/closeClose one session (code, reason)
POST /api/v1/realtime/endpoints/:id/close-allClose every session
GET /api/v1/realtime/endpoints/:id/streamFollow sessions and messages live as an SSE stream (curl -N)
GET /api/v1/realtime/endpoints/:id/sessions/:session_id/exportDownload a session as NDJSON, or JSON with ?format=json

Send and broadcast bodies take these fields:

  • payload: a string, or JSON that is encoded for you.
  • opcode: text or binary. A binary payload is base64.
  • event_type, event_id, retry_ms and render_template.
  • Protocol fields, described on each protocol page: namespace, spread_args and args for Socket.IO; topic, qos and retain for MQTT.
curl -X POST https://app.hooklistener.com/api/v1/realtime/endpoints/<id>/broadcast \
-H "Authorization: Bearer hklst_your_api_key" \
-H "Content-Type: application/json" \
-d '{"payload": {"type": "price", "value": 42}, "event_type": "tick"}'

AI coding assistants can do the same through the MCP server's realtime tools.