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 untuk kekal aktif. Hantar baris komen—sesuatu seperti : ping—setiap dua puluh saat. Komen dalam SSE diabaikan oleh pengendali mesej pelayar, tetapi ia memastikan sambungan TCP kekal aktif. Pengimbang beban (load balancers) dan CDN sering memutuskan sambungan senyap selepas tiga puluh atau enam puluh saat. Satu baris baharu yang ringkas dapat menyelamatkan anda daripada masalah tersebut.

Hormati tab pengguna. Apabila pelawat mengecilkan atau menyembunyikan tab, hentikan proses. Dengar visibilitychange dalam pelayar dan panggil eventSource.close(). Pelayan juga harus mengesan pemutusan sambungan klien dan menamatkan gelung (loop). PHP boleh menyemak connection_aborted() di dalam gelung. Jangan biarkan sambungan hantu membazirkan pekerja (workers) untuk orang yang telah pergi sepuluh minit yang lalu.

Had Maksimum: Pekerja PHP

SSE dalam PHP adalah jujur tentang hadnya. Setiap sambungan SSE yang terbuka menggunakan satu pekerja PHP. Jika pool anda mempunyai seratus pekerja, anda mempunyai seratus stream. Titik. Tiada jalan penyelesaian async selagi anda berada di dalam model proses Apache atau PHP-FPM. Anda boleh melaras pm.max_children, tetapi memori dan CPU menetapkan sempadan sebenar.

Had itu akan terasa cepat jika anda juga menggunakan pekerja untuk pemuatan halaman biasa, panggilan API, dan penjanaan aset. Pantau ketepuan pekerja anda dengan teliti. Jika endpoint SSE anda mula beratur (queuing) kerana semua pekerja terkunci dalam stream selama dua puluh minit, seluruh laman web anda akan menjadi perlahan.

Apabila angka tidak lagi mencukupi, beralihlah. Go adalah langkah seterusnya yang biasa, walaupun Rust, Node.js, atau Erlang boleh memainkan peranan yang sama. Goroutine dalam Go adalah kuncinya. Sebuah goroutine hanya menelan beberapa kilobait. Anda boleh menampung puluhan ribu stream pada perkakasan sederhana tanpa sebarang masalah. Logik teras kekal sama—baca dari cache, tulis ke socket—tetapi runtime bertukar daripada proses berat kepada thread yang ringan.

Namun, jangan bermula di situ. PHP boleh membawa anda sejauh yang tidak disangka. Sahkan produk anda terlebih dahulu. Apabila halaman metrik menunjukkan keletihan pekerja (worker exhaustion) dan bukannya beban lampau pangkalan data, bermakna anda telah melampaui stack tersebut. Itu adalah masalah yang baik.

Rumusan

Kaunter langsung (live counters) bukan tentang teknologi semata-mata. Ia adalah tentang melindungi pangkalan data anda daripada pengguna anda sendiri. Mulakan dengan SSE kerana ia lebih mudah daripada yang disangka. Gunakan cache secara agresif antara stream dan pangkalan data supaya jumlah sambungan tidak menjadi jumlah query. Pantau had pekerja anda dengan teliti. Dan mulakan dengan cara yang mudah. PHP sudah memadai sehinggalah ia tidak lagi mencukupi, dan pada masa itu anda akan tahu dengan tepat mengapa anda menulis semula kod tersebut.

Untuk projek anda yang seterusnya:

  • Gunakan SSE apabila data mengalir satu hala, dari pelayan ke pelayar.
  • Letakkan lapisan cache di hadapan pangkalan data. Satu query setiap beberapa saat adalah lebih baik daripada beribu-ribu.
  • Hadkan sambungan SSE kepada bawah satu minit dan biarkan pelayar menyambung semula.
  • Tutup stream apabila tab disembunyikan. Jangan bazirkan pekerja aktif untuk sambungan yang tidak aktif (idle).
  • Pantau penggunaan pekerja PHP. Apabila anda mencapai had maksimum, pindahkan lapisan penstriman ke Go.