מספר הצפיות הוא הנתון הראשון שבו מבקר סומך. הוא אומר לו אם סרטון שווה שלושים שניות או שלושים דקות מזמנו. ב-TopVideoHub, המספר הזה נע מהר. קליפ ויראלי יכול לצבור 40,000 צפיות תוך עשר דקות. אם המונה בדף קופא, החדר מרגיש ריק. משתמשים עוזבים (bounce).
להעביר את המספר הזה לדפדפן נשמע פשוט. זה לא. הפתרון הראשון שרוב הצוותים פונים אליו הוא polling. קל לחבר אותו וזה עובד מצוין בסביבת staging. אבל staging משקרת.
מתי polling הופך ל-DDoS נגד עצמך
צוות TopVideoHub בנה poller פשוט ב-JavaScript. הוא משך את מספר הצפיות העדכני בכל חמש שניות. בסביבת בדיקות עם שלושה דפדפנים פתוחים, זה נראה נהדר. ב-production, זה הרס את הפלטפורמה.
שמונה אלף צופים בו-זמנית שמרעננים כל חמש שניות יצרו 1,600 בקשות בשנייה. כל בקשה חדרת למסד הנתונים. ה-replication lag זינק. ה-read replicas נאנקו. שכבות ה-cache עוקפו. הצוות לא סיפק וידאו. הם סיפקו עומס שגרמו לעצמם.
polling הוא תמים עד שהוא לא. עבור דאשבורדים עם תעבורה נמוכה או פאנלים של מנהלים, זה בסדר. עבור דף של סרטון ויראלי, זה פצצה מתקתקת. הצוות היה זקוק לצינור קבוע מהשרת לדפדפן, אך הם לא היו זקוקים למורכבות של פרוטוקול full-duplex.
מדוע SSE מתאים לצינורות חד-כיווניים
Server-Sent Events (SSE) נבנה בדיוק עבור סוג כזה של בעיה: לשרת יש נתונים, והדפדפן רק צריך להקשיב.
בניגוד ל-WebSockets, SSE רץ על גבי HTTP רגיל. זה חשוב יותר ממה שזה נשמע. אין צורך בכללים חדשים ב-proxy, ב-upgrade headers או בגירוגי מניפולציות של load-balancer. אם השרת שלכם מדבר HTTP/1.1 או HTTP/2, SSE יעבוד. הניפוי (debugging) הוא ללא כאבים מכיוון שהזרם (stream) הוא פשוט טקסט. ניתן להפנות את curl ל-endpoint ולראות את המספרים רצים בזמן אמת, מה שעדיף על ניחושים מדוע מסגרת socket בינארית השתבשה.
הדפדפן מטפל בחלקים המשעממים בחינם. אם החיבור מתנתק, SSE מתחבר מחדש באופן אוטומטי עם ה-header של Last-Event-ID, כך שהשרת יודע מאיפה להמשיך. ממשק ה-API ב-JavaScript הוא זעיר: יוצרים EventSource, מצמידים handler של onmessage, וסיימתם.
Caching הוא הארכיטקטורה האמיתית
הטעות הארכיטקטונית הגדולה ביותר במונים חיים היא להתייחס לכל חיבור דפדפן כסיבה לשאול את מסד הנתונים. אם 8,000 אנשים צופים באותו סרטון, הרצת 8,000 שאילתות כל שתי שניות היא טירוף. מסד הנתונים שלכם לא ישרד תעבורה ויראלית.
TopVideoHub פתרה זאת באמצעות APCu, ה-in-memory opcode ו-user cache של PHP. הזרימה היא פשוטה. תהליך רקע — או endpoint קל שנקרא על טיימר — כותב את מספר הצפיות הנוכחי לתוך APCu פעם כל שתי שניות. ה-SSE endpoint, שאלפי דפדפנים עשויים להחזיק פתוח, קורא אך ורק מתוך APCu.
התוצאה: מסד הנתונים מקבל פגיעה אחת כל שתי שניות, לא משנה כמה צופים מחוברים. ה-cache הופך לבולם הזעזועים. APCu אינו משהו אקזוטי. הוא מגיע עם PHP, חי בזיכרון משותף (shared memory), וקורא מהר יותר מכל network round-trip. עבור מספר בודד שמשתנה בתדירות גבוהה אך לא באופן מיידי, זה הכלי הנכון.
אם אינכם מריצים APCu, גם Redis או Memcached יעבדו. העיקרון נשאר זהה. הפרידו את נתיב הקריאה החם (hot read path) ממסד הנתונים.
לשכנע את PHP, LiteSpeed ו-Cloudflare להזרים נתונים
PHP רוצה לסיים וללכת הביתה. שרתי אינטרנט רוצים לבצע buffering לפלט ולהעביר תגובה מסודרת. SSE זקוק להפך: חיבור שנשאר פתוח, ומשחרר (flushing) bytes ברגע שהם מגיעים. ללא זהירות, ה"זרם" שלכם יגיע כגוש אחד (blob) שלושים שניות מאוחר יותר, מה שיבטל את המטרה.
כך TopVideoHub שמרה על הצינור פתוח.
בטלו output buffering. בתחילת סקריפט ה-SSE, השביתו כל שכבת buffering ש-PHP עשוי היה להפעיל. קראו ל-ob_end_flush() אם קיים buffer פעיל, וכבו flushing מובנה באמצעות ob_implicit_flush(true) לאחר שליחת ה-headers שלכם.
הורו ל-proxies להפסיק buffering. שלחו את ה-header X-Accel-Buffering: no. Nginx מקשיב לו. LiteSpeed מקשיב לו. זה מאותת שהתגובה לא צריכה לעבור buffering או להתהדק לגוש שניתן לשמור ב-cache.
הגדירו זמן חיים קצר. כל חיבור SSE תופס PHP worker. TopVideoHub מגבילה את הזרמים ל-55 שניות. כשהטיימר מגיע לסיומו, השרת שולח comment סופי, סוגר את הזרם, והדפדפן מתחבר מחדש באופן אוטומטי. החיבור מחדש הזה נוחת על worker חדש, מה שמונע מכל תהליך בודד "להתנחל" לנצח.
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.
