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 to stay alive. Send a comment line—something like : ping—every twenty seconds. Comments in SSE are ignored by the browser’s message handler, but they keep the TCP connection warm. Load balancers and CDNs often drop silent connections after thirty or sixty seconds. A cheap newline saves you from that axe.
Respect the user’s tab. When a visitor minimizes or hides the tab, bail out. Listen for visibilitychange in the browser and call eventSource.close(). The server should also detect a client disconnect and terminate the loop. PHP can check connection_aborted() inside a loop. Do not let ghost connections burn workers for people who left ten minutes ago.
The Hard Ceiling: PHP Workers
SSE in PHP is honest about its limits. Every open SSE connection consumes one PHP worker. If your pool has a hundred workers, you have a hundred streams. Full stop. There is no async workaround while you are inside an Apache or PHP-FPM process model. You can tune pm.max_children, but memory and CPU set the real boundary.
That limit bites fast if you are also using workers for regular page loads, API calls, and asset generation. Monitor your worker saturation carefully. If your SSE endpoint starts queuing because all workers are locked in twenty-minute streams, your entire site slows down.
When the numbers no longer fit, move. Go is the usual next step, though Rust, Node.js, or Erlang can play the same role. Go’s goroutines are the key. A goroutine costs a few kilobytes. You can hold tens of thousands of streams on modest hardware without breaking a sweat. The core logic stays identical—read from cache, write to socket—but the runtime switches from heavy processes to lightweight threads.
Do not start there, though. PHP gets you surprisingly far. Validate the product first. When the metrics page shows worker exhaustion instead of database overload, you have outgrown the stack. That is a good problem.
Takeaway
Live counters are not about raw technology. They are about protecting your database from your own users. Start with SSE because it is simpler than it looks. Cache aggressively between the stream and the database so connection count does not become query count. Watch your worker limits like a hawk. And start simple. PHP is enough until it is not, and by then you will know exactly why you are rewriting.
For your next project:
- Use SSE when the data flows one way, from server to browser.
- Put a cache layer in front of the database. One query every few seconds beats thousands.
- Cap SSE connections to under a minute and let the browser reconnect.
- Close streams on tab hide. Do not fund idle connections with live workers.
- Monitor PHP worker usage. When you max out, port the streaming layer to Go.
