A view count is the first number a visitor trusts. It tells them whether a video is worth thirty seconds or thirty minutes of their time. At TopVideoHub, that number moves fast. A trending clip can rack up 40,000 views in ten minutes. If the counter on the page freezes, the room feels empty. Users bounce.

Getting that number to the browser sounds trivial. It is not. The first solution most teams reach for is polling. It is easy to wire up and it works fine in staging. But staging lies.

When Polling Becomes a DDoS Against Yourself

The TopVideoHub team built a simple JavaScript poller. It fetched the latest view count every five seconds. In a test environment with three browsers open, it looked great. In production, it cratered the platform.

Eight thousand concurrent viewers refreshing every five seconds generated 1,600 requests per second. Every request drilled into the database. Replication lag spiked. Read replicas groaned. Cache layers got bypassed. The team was not serving video. They were serving self-inflicted load.

Polling is innocent until it is not. For low-traffic dashboards or admin panels, it is fine. For a viral video page, it is a ticking bomb. The team needed a persistent pipe from server to browser, but they did not need the complexity of a full-duplex protocol.

Why SSE Fits One-Way Pipes

Server-Sent Events (SSE) is built for exactly this shape of problem: the server has data, and the browser only needs to listen.

Unlike WebSockets, SSE rides on plain HTTP. That matters more than it sounds. You do not need new proxy rules, upgrade headers, or load-balancer gymnastics. If your server speaks HTTP/1.1 or HTTP/2, SSE works. Debugging is painless because the stream is just text. You can point curl at the endpoint and watch numbers scroll by in real time, which beats guessing why a binary socket frame went sideways.

The browser handles the tedious parts for free. If the connection drops, SSE reconnects automatically with the Last-Event-ID header so the server knows where to resume. The API surface in JavaScript is tiny: create an EventSource, attach an onmessage handler, and you are done.

Caching Is the Real Architecture

The biggest architectural mistake in live counters is treating every browser connection as a reason to query the database. If 8,000 people watch the same video, running 8,000 queries every two seconds is madness. Your database will not survive viral traffic.

TopVideoHub solved this with APCu, PHP’s in-memory opcode and user cache. The flow is simple. A background process—or a lightweight endpoint hit on a timer—writes the current view count into APCu once every two seconds. The SSE endpoint, which thousands of browsers may be holding open, reads exclusively from APCu.

The result: the database takes one hit every two seconds, no matter how many viewers are tuned in. The cache becomes the shock absorber. APCu is not exotic. It ships with PHP, lives in shared memory, and reads faster than any network round-trip. For a single number that changes frequently but not instantaneously, it is the right tool.

If you are not running APCu, Redis or Memcached work too. The principle stays the same. Separate the hot read path from the database.

Convincing PHP, LiteSpeed, and Cloudflare to Stream

PHP wants to finish and go home. Web servers want to buffer output and deliver a neat response. SSE needs the opposite: a connection that stays open, flushing bytes as they arrive. Without care, your "stream" arrives as a single blob thirty seconds later, defeating the purpose.

Here is how TopVideoHub kept the pipe unclogged.

Kill output buffering. At the start of the SSE script, disable every layer of buffering PHP might have enabled. Call ob_end_flush() if a buffer is active, and turn off implicit flushing with ob_implicit_flush(true) after your headers are sent.

Tell proxies to back off. Send the X-Accel-Buffering: no header. Nginx listens to it. LiteSpeed listens to it. It signals that the response should not be buffered or compressed into a cacheable lump.

Set a short lifetime. Each SSE connection ties up a PHP worker. TopVideoHub caps streams at 55 seconds. When the timer hits, the server sends a final comment, closes the stream, and the browser reconnects automatically. That reconnection lands on a fresh worker, preventing any single process from squatting forever.

Ping om in leven te blijven. Stuur elke twintig seconden een commentaarregel—iets als : ping. Comments in SSE worden genegeerd door de message handler van de browser, maar ze houden de TCP-verbinding warm. Load balancers en CDN's verbreken vaak stille verbindingen na dertig of zestig seconden. Een simpele newline bespaart je van die strop.

Respecteer het tabblad van de gebruiker. Wanneer een bezoeker het tabblad minimaliseert of verbergt, stop dan direct. Luister naar visibilitychange in de browser en roep eventSource.close() aan. De server moet ook een client-disconnectie detecteren en de loop beëindigen. PHP kan connection_aborted() controleren binnen een loop. Laat 'ghost connections' geen workers verspillen voor mensen die tien minuten geleden al weg zijn.

Het harde plafond: PHP-workers

SSE in PHP is eerlijk over zijn limieten. Elke open SSE-verbinding verbruikt één PHP-worker. Als je pool honderd workers heeft, heb je honderd streams. Punt uit. Er is geen async workaround zolang je binnen een Apache- of PHP-FPM-procesmodel werkt. Je kunt pm.max_children aanpassen, maar geheugen en CPU bepalen de werkelijke grens.

Die limiet wordt snel een probleem als je workers ook gebruikt voor reguliere paginaloads, API-aanroepen en het genereren van assets. Monitor de verzadiging van je workers nauwgezet. Als je SSE-endpoint in een wachtrij terechtkomt omdat alle workers vastzitten in streams van twintig minuten, vertraagt je hele site.

Wanneer de cijfers niet langer passen, moet je overstappen. Go is meestal de volgende stap, hoewel Rust, Node.js of Erlang dezelfde rol kunnen spelen. De goroutines van Go zijn de sleutel. Een goroutine kost slechts enkele kilobytes. Je kunt tienduizenden streams draaien op bescheiden hardware zonder moeite. De kernlogica blijft identiek—lezen uit de cache, schrijven naar de socket—maar de runtime schakelt over van zware processen naar lichtgewicht threads.

Begin daar echter niet mee. PHP brengt je verrassend ver. Valideer eerst het product. Wanneer de metrics-pagina worker-uitputting laat zien in plaats van database-overbelasting, ben je de stack ontgroeid. Dat is een goed probleem.

Kernpunten

Live counters gaan niet over pure technologie. Ze gaan over het beschermen van je database tegen je eigen gebruikers. Begin met SSE omdat het eenvoudiger is dan het lijkt. Cache agressief tussen de stream en de database, zodat het aantal verbindingen niet het aantal queries wordt. Houd je worker-limieten nauwlettend in de gaten. En begin simpel. PHP is voldoende totdat het dat niet meer is, en tegen die tijd weet je precies waarom je aan het herschrijven bent.

Voor je volgende project:

  • Gebruik SSE wanneer de data één kant op stroomt, van server naar browser.
  • Plaats een cache-laag voor de database. Eén query per paar seconden is beter dan duizenden.
  • Beperk de duur van SSE-verbindingen tot minder dan een minuut en laat de browser opnieuw verbinden.
  • Sluit streams wanneer een tabblad wordt verborgen. Verspil geen actieve workers aan inactieve verbindingen.
  • Monitor het gebruik van PHP-workers. Wanneer je het maximum bereikt, migreer je de streaming-laag naar Go.