Engineering
Twitch IRC vs EventSub: when each one wins
Twitch has two real-time APIs that overlap awkwardly. IRC is the old, weird, anonymous-friendly chat protocol. EventSub is the new, well-documented webhook/websocket system. The right answer for an overlay toolkit is to run both — and to know exactly which one owns which event.
The split, as it stands in 2026
Here's what each API actually carries:
- IRC — chat messages (PRIVMSG), sub events (USERNOTICE with msg-id
sub/resub/subgift/submysterygift), cheers (PRIVMSG with bits tag), raid receive (USERNOTICE withraid), CLEARCHAT, ROOMSTATE. - EventSub — channel.follow v2 (IRC never had follows), channel.update (title / category / language), raid send, channel point redemptions, hype train, polls, predictions. Everything Twitch added since ~2020.
The split isn't clean. Sub events technically come through both — there's an EventSub channel.subscribe topic — but IRC's USERNOTICE arrives faster and with the actual sub message body the user typed, which is what you want on screen.
The architecture we landed on
For the toolset, two long-lived connections per active overlay:
- Anonymous IRC connects as
justinfan<random>and joins the channel. No OAuth, no scopes, no token to refresh, no rate-limit concerns at our scale. Reads PRIVMSG + USERNOTICE. - EventSub websocket with the streamer's user token. Subscribes to channel.follow v2, channel.raid, channel.update. Re-subscribes on connect.
Why anonymous IRC instead of a bot account
The traditional pattern is "create a bot Twitch account, OAuth it, store the token, log it into IRC, /join the streamer's channel." That works. It also means:
- A token to keep alive and refresh
- An account to keep verified
- A bot user counted in the channel's chatter list
- A rate-limit budget shared across every streamer using the toolset
justinfan<random> is Twitch's documented anonymous-IRC handshake. You join chat read-only, you don't appear in the chatter list, and the rate limit on auth attempts is 20 per 10 seconds per user — and since the random suffix means every reconnect is a different "user," we never contest the limit even during a 100-stream OBS-restart storm.
The only thing anonymous IRC can't do is send messages. The toolset uses the streamer's own Twitch token for the rare moments we need to post (timer alerts, !sr confirmations) — sent through Helix chat-send, not IRC.
Why EventSub websocket, not webhook
EventSub has two transports: webhook (Twitch POSTs to your server) and websocket (your server connects out to Twitch). A subtle but load-bearing detail:
- Webhook EventSub requires an app access token to subscribe. The app — your published Twitch developer app — owns the subscription.
- Websocket EventSub requires a user access token at subscribe time. The user owns the subscription; it dies when their token does.
For a per-streamer overlay, websocket is the right choice — the streamer authorized us, we use their token, the subscription is naturally scoped to their channel. Webhook would require us to host a public callback endpoint and proxy events back to per-streamer DOs, which is more moving parts.
The channel.follow v2 trap
channel.follow v2 — the only version that still works in 2026 — has a different subscription condition than v1. You need both:
{
type: "channel.follow",
version: "2",
condition: {
broadcaster_user_id: <streamer-id>,
moderator_user_id: <streamer-id>, // <-- the v1 version didn't need this
}
}And the user token has to carry the moderator:read:followers scope. Miss either and you get a 400 with a deeply unhelpful message. We've hit that one in production exactly once and never want to again.
Where IRC's quirks bite
IRC USERNOTICE for gift-sub bombs sends both a submysterygift "X just gifted 10 subs!" message and N individual subgift messages for each recipient. If your overlay naively listens for "sub events" and renders one per message, you get 11 popups for a 10-sub bomb.
That's not an IRC library bug — Twitch sends both. The dedupe responsibility is yours: when a submysterygift arrives, suppress the next N subgift messages from the same gifter within a short window. The toolset's event-list overlay does this by default.
What we don't use
PubSub — deprecated. EventSub is the replacement and Twitch has been turning topics off.
Helix polling — for things like "is this streamer live?", we cache hard. Polling Helix every 30s for every active overlay would burn the streamer's rate-limit budget for no reason; we only call Helix on state-change moments.
Operating it
Both connections live inside the per-overlay Durable Object described in Hot-swap overlay config. When the DO wakes (overlay reconnects or config changes), it opens IRC + EventSub if they aren't open. When the DO evicts (no clients connected for a while), both connections close. That maps cleanly onto streamer-active time — no permanent connections eating connection budget while a streamer is asleep.
The event list overlay is the most visible product surface of this dual-connection architecture: every IRC USERNOTICE and EventSub channel.follow / channel.raid arrives at the same overlay through different doors, dedupe-and-render.