Twitch Chat Firehose
A public, real-time stream of chat messages and moderation events from the Twitch channels CatQuery currently monitors.
Watch it live
CatQuery Chat
Open chat.catquery.comConnect a client
The default stream (no query parameters, and explicitly ?jsonBasic=true or ?json=true) sends the structured JSON payload described below. Add ?raw=true to receive the original Twitch IRC wire line as plain text instead. Each of these also accepts =1 in place of =true.
Browser JavaScript (JSON, the default)
const socket = new WebSocket("wss://firehose.catquery.com/firehose");
socket.addEventListener("open", () => {
console.log("Connected to the CatQuery firehose");
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
console.log(`#${message.channel} <${message.displayName}> ${message.text}`);
});
socket.addEventListener("close", () => {
console.log("Disconnected; reconnect with backoff if needed");
});
Node.js with ws (raw IRC)
import WebSocket from "ws";
const socket = new WebSocket("wss://firehose.catquery.com/firehose?raw=true");
socket.on("message", (data) => {
console.log(data.toString());
});
Payload format
By default (and with ?jsonBasic=true or ?json=true), every WebSocket message is one JSON object with this shared shape. With ?raw=true, the server instead sends the raw IRC line as plain text.
{
"text": "HOORAY",
"username": "foobar",
"displayName": "FooBar",
"channel": "xqc",
"timestamp": "2025-11-22T06:11:26.537Z",
"id": "8939f358-4118-4fb6-86ee-4bba89a1de8e",
"tags": {
"color": "#000000",
"badges": "subscriber/12",
"emotes": "25:0-4",
"room-id": "29400754",
"user-id": "60458761",
"first-msg": "0"
}
}
| Field | Type | Meaning |
|---|---|---|
text | string | Chat text or a human-readable event description. |
username | string, optional | Lowercase Twitch login. It can be absent on moderation events. |
displayName | string | The sender's display name, or the affected username for a moderation event. |
channel | string | Lowercase channel login, without #. |
timestamp | string | UTC timestamp in ISO 8601 format. |
id | string | Twitch message ID. Moderation events currently use an empty string. |
tags | object | String-valued Twitch IRC tags. Available keys depend on the event type; missing optional values may be empty strings. |
Useful chat tags include badges, badge-info, color, emotes, first-msg, room-id, and user-id. Treat unknown tags as forward-compatible additions.
Event types
Chat messages
Normal Twitch PRIVMSG events have a non-empty id, a username, and the standard chat tags shown above.
User notices
Subscriptions, raids, announcements, and similar Twitch USERNOTICE events have tags["event-type"] === "usernotice". When system-msg is nonempty, text starts with that system message, followed by one separator space if user-entered text is present. Announcements with an empty system-msg contain only the user-entered text, with its whitespace preserved. Original Twitch tags and emote positions are retained; emote positions refer to the user-entered text, excluding any system-message prefix.
Timeouts and permanent bans
Moderation CLEARCHAT events have an empty id, no username, and include target-user-id. A non-empty ban-duration means a timeout; an empty value means a permanent ban.
function eventType(event) {
if (event.tags["event-type"] === "usernotice") return "usernotice";
if (event.id === "" && event.tags["target-user-id"]) {
return event.tags["ban-duration"] ? "timeout" : "ban";
}
return "message";
}
Usage and reliability notes
- This is a live broadcast, not a history API. Events sent before you connect are not replayed.
- The stream covers channels monitored by CatQuery; it is not every message sent across all of Twitch.
- Connections can close during deployments, network interruptions, or if a client cannot consume data quickly enough. Reconnect with exponential backoff and jitter.
- Each IP address is limited to ten simultaneous connections and 100 new connections per minute. Exceeding either limit triggers escalating temporary blocking.