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.
Hayatta kalmak için ping atın. Her yirmi saniyede bir : ping gibi bir yorum satırı gönderin. SSE'deki yorumlar tarayıcının mesaj işleyicisi tarafından görmezden gelinir, ancak TCP bağlantısını sıcak tutarlar. Yük dengeleyiciler (Load balancers) ve CDN'ler genellikle otuz veya altmış saniye sonra sessiz bağlantıları keser. Basit bir yeni satır karakteri sizi bu durumdan kurtarır.
Kullanıcının sekmesine saygı duyun. Bir ziyaretçi sekmeyi küçülttüğünde veya gizlediğinde işlemi sonlandırın. Tarayıcıdaki visibilitychange olayını dinleyin ve eventSource.close() fonksiyonunu çağırın. Sunucu da istemci bağlantısının kesildiğini algılamalı ve döngüyü sonlandırmalıdır. PHP, bir döngü içinde connection_aborted() fonksiyonunu kontrol edebilir. Hayalet bağlantıların, on dakika önce ayrılmış kişiler için worker'ları tüketmesine izin vermeyin.
Sert Sınır: PHP Worker'ları
PHP'de SSE, limitleri konusunda dürüsttür. Açık olan her SSE bağlantısı bir PHP worker'ı tüketir. Eğer havuzunuzda yüz worker varsa, yüz akışınız (stream) var demektir. Nokta. Apache veya PHP-FPM işlem modeli içindeyken asenkron bir çözüm yolu yoktur. pm.max_children değerini ayarlayabilirsiniz ancak asıl sınırı bellek ve CPU belirler.
Eğer worker'ları normal sayfa yüklemeleri, API çağrıları ve varlık (asset) üretimi için de kullanıyorsanız, bu limit sizi hızla zorlar. Worker doluluğunu dikkatle izleyin. Eğer tüm worker'lar yirmi dakikalık akışlara kilitlendiği için SSE uç noktanız (endpoint) kuyruğa girmeye başlarsa, tüm siteniz yavaşlar.
Sayılar artık yetmediğinde, geçiş yapın. Rust, Node.js veya Erlang de aynı rolü üstlenebilse de, genellikle bir sonraki adım Go olur. Go'nun goroutine'leri işin anahtarıdır. Bir goroutine sadece birkaç kilobayt yer kaplar. Mütevazı donanımlarda bile hiç zorlanmadan on binlerce akışı tutabilirsiniz. Temel mantık aynı kalır—önbellekten oku, sokete yaz—ancak çalışma zamanı (runtime) ağır süreçlerden (processes) hafif iş parçacıklarına (threads) dönüşür.
Yine de işe oradan başlamayın. PHP sizi şaşırtıcı derecede ileriye taşır. Önce ürünü doğrulayın. Metrik sayfası, veritabanı aşırı yüklenmesi yerine worker tükenmesi gösterdiğinde, mevcut yığınınızın (stack) ötesine geçmişsiniz demektir. Bu iyi bir sorundur.
Özet
Canlı sayaçlar sadece ham teknolojiyle ilgili değildir. Veritabanınızı kendi kullanıcılarınızdan korumakla ilgilidir. SSE ile başlayın çünkü göründüğünden daha basittir. Bağlantı sayısının sorgu sayısına dönüşmemesi için akış ile veritabanı arasında agresif bir şekilde önbellekleme (cache) yapın. Worker limitlerinizi bir şahin gibi izleyin. Ve basit başlayın. PHP, yetmeyene kadar yeterlidir; o noktaya geldiğinizde ise neden yeniden yazdığınızı tam olarak biliyor olacaksınız.
Bir sonraki projeniz için:
- Veri tek yönlü, sunucudan tarayıcıya akıyorsa SSE kullanın.
- Veritabanının önüne bir önbellek (cache) katmanı koyun. Birkaç saniyede bir yapılan tek bir sorgu, binlercesinden daha iyidir.
- SSE bağlantılarını bir dakikanın altında sınırlayın ve tarayıcının yeniden bağlanmasına izin verin.
- Sekme gizlendiğinde akışları kapatın. Boştaki bağlantıları canlı worker'lar ile beslemeyin.
- PHP worker kullanımını izleyin. Kapasiteye ulaştığınızda, akış katmanını Go'ya taşıyın.
