← Back to blog
ENGINEERING

How we hold 400 viewers without drops

StreamHive engineering·Feb 12, 2026·10 min

One host per session, a spare for the viewer poll, and why retention smoothing beats a bigger pool.

Viewer count on Kick is eventually consistent. The public endpoint you see in a browser tab — https://kick.com/api/v2/channels/<slug> — is the same one we poll, through a reserved host, roughly every five seconds, on a background thread that never blocks spawn. That one detail matters more than almost anything else we do: the number you care about is not how many connections we have open. It is the number Kick chooses to show. So we measure ourselves against Kick, and nothing else.

The trick is not more proxies. It is one unique host per session, no sharing, and a spare host left idle so the viewer poll never competes with a connect. Add hunters that each own a semaphore slot and a host inside the worker, reap the dead ones so their hosts can be reused, and refill twenty at a time so the control loop never stalls. When we say 'no drops,' we do not mean we have never lost a socket. We mean the run is shaped so one failure does not cascade into a room full of them.

Why one host per session? Because Kick counts viewers, in part, by who is asking. Ten sockets behind one residential IP read as ten tabs from one person, not ten people. Ten sockets behind ten distinct IPs read as ten viewers. That is the entire geometry of a hold. Everything downstream — the pool, the hunters, the spare host — exists to protect that one invariant: no two sessions share an address.

The spare host is the quiet hero. The viewer poll is a Kick HTTP call that has to go out through a proxy like everything else. If the poll grabs the same host a connect is using, two things can happen: the connect slows, or the poll returns a bad answer. We keep one unique host out of rotation and dedicate it to the poll. The poll alternates between the v2 and v1 endpoints — one GET per tick, not two — and it runs off-thread with a short timeout. 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 and the dashboard's viewer number along with it. Moving the poll off-thread was the single biggest retention win we shipped.

Retention smoothing is the other half. A flat 250 for three hours reads more real than a sawtooth of connects and disconnects. We hold sockets until you stop them or the host dies — no short-lived ping cap — and we stage ramps twenty per tick so the count climbs without a spike. A spike is a fingerprint. Twenty per tick is slow enough to look human and fast enough to feel responsive when you watch the deck.

Here is the honest part, the part most viewer-bot copy leaves out: sockets do not always convert. A proxy can fetch the token and hold a websocket and still never register as a viewer on Kick's side. We have watched a hold where the deck showed forty active connections and Kick's public count moved by two. This is not a bug we can always fix; it is the nature of the platform. Kick is the arbiter. When our dashboard and Kick disagree, we trust Kick and treat the gap as a signal, not a cosmetic error we paper over.

That is why Live Viewers on the ops deck is Kick's viewer_count, fetched through a reserved host — not a count of our own sockets. Counting sockets would flatter us every time a connection held but did not convert. We would rather show you the number your audience actually sees, even when it is humbling. A dashboard that reports what Kick reports is the only dashboard you can actually verify against the tab you already have open.

Spawn discipline matters because the pool is the ceiling. Hunters acquire their own slot and host inside the worker; a dead hunter is reaped so its host goes back into rotation. We cap new spawns at twenty per tick. That cap is not arbitrary — it is the difference between a ramp and a thundering herd that saturates the pool, the token endpoint, and the control loop all at once. Without it, spawn, refill, and the viewer poll all fight over the same resources, and the count freezes.

There are no fake channel IDs in this system. If Kick does not resolve your slug to a channel id, the bot stops. We do not pad a slug, do not substitute a fallback id, and do not run against a channel you did not name. That sounds small; it is actually a trust property. When the run reports a count, it is a count for your channel, measured by Kick — not a number we invented to make the dashboard look busy.

You can test all of this on a quiet channel. Start twenty-five, watch Kick's viewer_count on the public API, then step to a hundred. Note the baseline before you start and the peak after sockets come up. If the deck and Kick disagree, trust Kick and tell us — that is a poll bug, not a feature. In our own live checks on low-viewer channels we have watched a count climb from a handful into the dozens after sockets came up, and we treat that lift, not the dashboard's self-report, as the pass criterion.

A note on the terms, because it belongs in every one of these posts. Inflating viewer numbers violates most platforms' terms of service, and Kick is no exception. Nothing about unique hosts or retention smoothing makes a hold compliant. What those things do is make the run look less like a block of identical viewers arriving at once. That is a realism question, not a safety question. Anyone who tells you otherwise is selling you something.

Drops, then, are not something you eliminate — they are something you design around. A host dies, its hunter is reaped, its slot frees, the next tick refills from the pool, and the count dips for a poll or two before it recovers. The difference between a hold that 'drops' and one that holds is whether that recovery is visible to your audience or absorbed by the ramp. Flat curves are not luck; they are a recovery path that is faster than Kick's own smoothing window.

Before any socket can hold, it has to ask Kick for a token, and that step is easy to get wrong in ways that stall the whole ramp. We fetch the token fail-fast with a three-second timeout and no homepage warmup. The token is the X-CLIENT-TOKEN header and nothing else — the cookies, the warmup GET, the pretend-human sleep, none of that is what Kick uses to recognize a viewer. Cutting the warmup means a dead proxy fails in three seconds instead of dragging the token queue for minutes while hunters wait. That is the difference between a ramp and a freeze.

Two numbers on the dashboard look alike and mean different things, and confusing them is how people mistake sockets for viewers. Active Bots counts live websockets — connections we opened. Live Viewers counts Kick's public viewer_count — what your audience sees. They diverge whenever a socket holds but never converts, which happens. We show both and label them, because a dashboard that lets you conflate them is a dashboard that flatters you into trusting a number Kick does not agree with.

A hold beats a spike, and it is worth saying why, because the temptation is always to arrive fast. Kick's count smooths over a window, and an audience reads a steady room as a real room. A burst of two hundred that appears at once is the shape of a bot farm; a ramp that climbs twenty at a time over a minute reads like people trickling in. The algorithm and the audience both see the same curve, and both punish the spike. Retention smoothing is not cosmetics; it is the whole point.

What a fill looks like in the log, if you have never watched one: sockets come up over a minute or two, Active climbs toward the requested count as hosts allow, and Kick's public number follows a poll or two behind. The gap between 'Active is full' and 'Kick agrees' is measured in polls, not minutes. If Kick never moves after sockets are up, that is a failed run — and the honest thing is to say so and diagnose, not to call the dashboard done because Active looks busy.

The loop that keeps all of this alive is deliberately boring, and boring is the feature. Every tick it checks who is dead, reaps their hunter, frees the slot, and refills from the pool, capped at twenty new workers so spawn never blocks the poll or the token endpoint. That is why the count keeps moving even while the pool is busy. A loop that tries to spawn everything at once is a loop that stops answering the dashboard, and a dashboard that stops answering looks like a drop even when every socket is fine.

So the recipe for four hundred without visible drops is: one host per session, a spare for the poll, hunters that own their slot, refills capped at twenty, sockets held until death, and a dashboard that measures Kick instead of itself. It is less glamorous than 'unlimited viewers,' and it is the only version that holds up when you actually watch the public count.

Join the waitlist

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

Join the waitlist →