← Back to blog
PRODUCT

Reading Kick like your audience does

StreamHive engineering·Mar 22, 2026·9 min

Live Viewers comes from the proxied public API, not from counting our own sockets. Here's why.

The ops deck's Live Viewers is not a count of our connections. It is Kick's own viewer_count, fetched through a reserved unique host every few seconds. If the two disagree, the ops deck is wrong, and we treat that as a poll bug, not as a number we are allowed to paper over. Your audience does not see our sockets. They see Kick.

That is deliberate. Viewers are what Kick shows on the channel page and on https://kick.com/api/v2/channels/<slug>, and that is the number that decides whether a hold worked. Counting sockets would flatter us whenever a proxy fetches a token, holds a websocket, and never converts. We have watched runs where Active showed forty and Kick moved by two. Calling that forty viewers would be lying, including to ourselves.

Two labels on the deck exist so you cannot conflate them. Active Bots is live Kick websockets, climbing toward the requested count as unique hosts allow. Live Viewers is the proxied public count. Requests are Kick attempts — token fetch, websocket connect, ping, channel lookup — not a socket total. Unique Hosts are distinct TCP-live IPs; Live Proxies are TCP-live lines. If a dashboard lets you mix those up, it is a dashboard that will talk you into trusting a number Kick does not agree with.

The poll runs off-thread on purpose. It used to run inline on the control loop, and in one measured session it blocked for thousands of seconds while proxies were slow — freezing spawn, refill, and the dashboard's viewer number along with it. Now `_viewer_poll_loop` is a daemon thread with a short timeout, one GET per tick, alternating the v2 and v1 endpoints so a tick never pays for both. The number moves even when spawn is busy.

One spare unique host is held out of rotation for that poll. Channel, token, viewer, and chat Kick calls walk reserved hosts; the poll cannot steal the same address a connect is using. If it did, two things happen: the connect slows, or the poll returns a bad answer. A captive portal, an error envelope, or the wrong channel is rejected up front and never counted as 'the stream went offline.' Only a 200 for our channel with a null livestream counts as a strike.

That last part is how you read Kick like an operator instead of like a log. A free proxy that returns someone else's page is not proof your channel died. Three consecutive real offline confirmations stop the bot; timeouts and non-200s reset nothing and add nothing. The deck then reports stopped with 'Stream ended - bot stopped' instead of pretending to still be running against a dead room.

There are no fake channel IDs. If Kick does not resolve your slug, the bot stops. We do not pad the slug, do not invent a fallback id, and do not run against a channel you did not name. When Live Viewers moves, it is moving for the channel in the URL bar you already have open. That is the only verification that counts: the same JSON your audience's client can fetch.

How to read a live run, in order. Record the slug and Kick's viewer_count before Start. Confirm the backend resolved that slug to Kick's id — not a different channel. Watch until Kick's public count is clearly above the baseline, not until Active looks busy. If Kick does not move after sockets are up, the run failed. Dashboard-only numbers are not a pass.

Lag of one poll is normal. Kick is eventually consistent, and our poll is ~5s. A sample that shows Kick at 49 and the deck at 12 is a poll behind, not a different universe — wait one tick before you declare a bug. A sample that shows Active at 40 and Kick still at the baseline after sockets have been up is the universe where conversion failed, and that is a failed run.

The honesty note is the same one we put on every engineering post, because a product post that skips it is a product post that is selling a ban. Inflating viewer numbers violates Kick's terms of service. Showing you Kick's own count instead of ours does not make a hold compliant. It makes the deck honest about what the audience sees. Honesty about the number is not the same as legality of the number.

Chat is a separate Socket.IO surface and a separate host budget. Do not read chat activity as proof of viewers, or viewers as proof of chat. A warm room on its own IPs can exist with a small count; a large count can exist with a silent room. Mixing the two signals on one address is how a hold starts to look staged. Read them apart, the way Kick's systems are apart.

System metrics on the deck — CPU, memory, network — should update. Frozen zeros after connect mean the browser is looking at a dead socket, not at a quiet machine. Status should be RUNNING with Stop Bot once sockets are up, not stuck on Starting or LOADING_PROXIES. Those are operator tells, not audience tells, but they are how you know the number you are reading is still being produced.

Free-proxy refresh is off-thread and cannot overwrite a running run. An empty refresh must not flip Status to ERROR while sockets are up. If you are reading Kick through a reserved host, a pool refresh that fails is a pool problem, not a channel problem. Do not stop a hold because the free list had a bad minute.

A practical check you can do without us. Open the channel page and the v2 API in another tab. Start twenty-five on a live low-viewer channel. Watch Kick's viewer_count, then glance at Live Viewers. They should track. Peak and Average on the viewers-over-time chart should be that same series, not a stale first lookup. If the chart is stuck on the first payload, you are not reading Kick anymore — you are reading a cache we failed to refresh.

Why we keep saying 'reserved unique host' instead of 'a proxy': Kick counts, in part, by who is asking. Ten sockets behind one residential IP read as ten tabs from one person. Ten sockets behind ten distinct IPs read as ten viewers. The poll has to go out through a proxy like everything else, so it needs its own host, or it starts to look like the hold talking to itself. Unique-host hygiene is how both the fill and the measurement stay in the same geometry.

Rotating residential or mobile gateways are the wrong tool for this read. A rotating gateway hands you a different IP per request, which sounds like diversity until the hold needs the same IP for the life of a session and the poll needs a stable spare. Dedicated ISP lists give you that; rotating resi and mobile often collapse through one carrier gateway and look like a single host. If Live Viewers never tracks Kick on a rotating list, believe Kick, then change the list.

The north star is the tab your audience already has open. Not Active, not Requests, not Unique Hosts — those are how we debug. Live Viewers is how we agree with Kick. If you only have time to watch one number during a run, watch Kick's viewer_count, and keep the deck around to see whether we are tracking it. That is reading Kick like your audience does.

Stream-offline is the other Kick-shaped signal on the deck. We only count a strike when Kick answers 200 with a body that is our channel and livestream is null. A mismatched payload is rejected and never touches the counter, so one bad free proxy cannot fake an ended stream. After three consecutive real offline confirmations the bot self-stops. If Live Viewers is still ticking while the channel page is clearly offline, you are not reading Kick — you are reading a poll that has not been allowed to believe the room ended.

Requests climbing while Live Viewers is flat is also a Kick-shaped read. Requests are attempts: token fetch, websocket connect, ping, channel lookup. A busy request counter with a frozen public count is conversion failing, not the poll dying. The instinct is to restart. The better read is: sockets are trying, Kick is not counting them, change hosts or stop calling it a hold. Restarting a non-converting run just reprints the same disagreement faster.

Keep the two session log files when you verify. The backend file should show the channel payload, Kick viewer_count A → B, running not overwritten by loading_proxies, and spawn not blocking the 5s poll. The frontend file should exist for the same session. If you only have a screenshot of Active, you do not have a Kick read — you have a socket read, which is the thing this post is telling you not to treat as the audience.

So: poll off-thread, one GET per tick, spare unique host, no fake channel ids, Active is not Live Viewers, and when they disagree Kick wins. The product is not a socket counter with a Kick skin. The product is a deck that is willing to look worse than the sockets when Kick has not converted them yet — because that is the only number that was ever real.

Join the waitlist

The blog explains the curve. The waitlist is how you get a batch when the ops deck opens.

Join the waitlist →