Server-Sent Events: הפתרון הפשוט לזרימת מידע חד-כיוונית בזמן אמת
מאת צוות מדיה דיל · 06.08.2026 · API Engineering · 7 דק׳
מדריך מעמיק ל-Server-Sent Events (SSE): איך זה עובד מעל HTTP רגיל, מתי עדיף על WebSocket, ואילו מגבלות ברוקסי ובדפדפן חייבים להכיר לפני שבונים איתו בפרודקשן.
Server-Sent Events (SSE) הוא אחד המנגנונים הכי לא מוערכים בעולם ה-Real-Time. בזמן שכולם רצים ל-WebSocket כברירת מחדל לכל דבר שדורש "עדכון חי", SSE מספק פתרון פשוט משמעותית לתרחיש נפוץ מאוד: זרימת מידע חד-כיוונית מהשרת ללקוח, מעל HTTP רגיל, בלי Handshake מיוחד, בלי Protocol Upgrade, ועם Reconnection מובנה בדפדפן שרוב מפתחי WebSocket בונים ידנית מאפס. הבנת מתי SSE הוא הבחירה הנכונה — ומתי הוא לא — חוסכת המון מורכבות מיותרת.
איך SSE עובד בפועל
SSE מבוסס על תכונה פשוטה של HTTP: תגובה שלא נסגרת. הלקוח פותח בקשת GET רגילה עם כותרת `Accept: text/event-stream`, השרת מגיב עם `Content-Type: text/event-stream` ופשוט לא סוגר את החיבור — הוא ממשיך לכתוב נתונים לזרם התגובה לאורך זמן, וכל "אירוע" הוא בלוק טקסט בפורמט מוגדר, מופרד בשורה ריקה:
data: {"type":"price_update","symbol":"AAPL","price":192.44}
data: {"type":"price_update","symbol":"MSFT","price":415.02}
: this is a comment, used as keep-alive
id: 42
event: notification
data: {"message":"הזמנה חדשה התקבלה"}
הדפדפן חושף API ייעודי בשם `EventSource` שמטפל בפרסור הפורמט הזה אוטומטית, כולל ניתוק וחיבור מחדש. זה ההבדל המהותי מ-WebSocket: אין פרוטוקול חדש, אין Upgrade, זו בקשת HTTP רגילה שנשארת פתוחה — מה שאומר שכל תשתית שכבר יודעת להתמודד עם HTTP (Load Balancers, CDNs, Proxies) עובדת איתה בלי קונפיגורציה מיוחדת, בניגוד ל-WebSocket שדורש תמיכה מפורשת ב-Upgrade header לאורך כל השרשרת.
שימוש ב-EventSource מהצד הלקוח
הצד הלקוח פשוט עד מדהים בהשוואה ל-WebSocket, שדורש טיפול ידני בכל אירועי ה-lifecycle:
const source = new EventSource('/stream');
source.addEventListener('notification', (e) => {
const data = JSON.parse(e.data);
showNotification(data.message);
});
source.onerror = () => {
// הדפדפן כבר מנסה להתחבר מחדש אוטומטית ברקע
console.log('Connection lost, browser is retrying...');
};
אין צורך לממש לוגיקת Backoff, אין צורך לעקוב אחרי מצב החיבור באופן ידני, ואין צורך לטפל ב-Race Conditions של ניסיונות חיבור מקבילים — כל זה כלול בממשק הדפדפן הסטנדרטי. זה ההבדל המרכזי בעלות הפיתוח בין SSE ל-WebSocket: לא רק בזמן הכתיבה הראשוני, אלא בכמות הבאגים שנחסכים לאורך חיי המוצר סביב תרחישי קצה של ניתוק וחיבור מחדש.
Reconnection אוטומטי: היתרון הכי נשכח
הפיצ'ר שמפתחי WebSocket משקיעים הכי הרבה קוד בבנייתו — Reconnection עם Backoff כשהחיבור נופל — מגיע חינם עם `EventSource`. אם החיבור נסגר מכל סיבה (Timeout, נפילת שרת, בעיית רשת), הדפדפן מנסה להתחבר מחדש אוטומטית, ואם השרת שלח כותרת `id:` באירוע האחרון שנקלט, הדפדפן שולח אותו בחזרה בכותרת `Last-Event-ID` בבקשת ה-Reconnect — מה שמאפשר לשרת לדעת בדיוק איפה הלקוח הפסיק ולשלוח רק את מה שהוא פספס:
app.get('/stream', (req, res) => {
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
const lastEventId = Number(req.headers['last-event-id'] || 0);
const missed = getEventsSince(lastEventId); // שליפה מהיסטוריה
missed.forEach(e => sendEvent(res, e));
const sub = subscribeToLiveEvents(e => sendEvent(res, e));
req.on('close', () => sub.unsubscribe());
});
המנגנון הזה, שדורש בנייה ידנית ב-WebSocket, הוא חלק מהפרוטוקול ב-SSE — וזה בדיוק סוג הפרט שגורם ל-SSE להיות פתרון בשל יותר ממה שנדמה למי שמכיר אותו רק בשמו.
פורמט האירועים לעומק: שדות מיוחדים
מעבר לשדה `data`, פורמט ה-SSE תומך בכמה שדות ייעודיים ששווה להכיר. `event:` מגדיר שם אירוע מותאם אישית, שמאפשר ללקוח להירשם ל-Listener ספציפי (`addEventListener('price_update', ...)`) במקום לטפל בכל ההודעות דרך `onmessage` גנרי ולפענח את הסוג בעצמו. `id:` מסמן מזהה ייחודי לאירוע, שמשמש הן לזיהוי (Debug, לוגים) והן — כפי שתואר — לחידוש הזרם מהנקודה הנכונה אחרי ניתוק. `retry:` מאפשר לשרת להורות לדפדפן על משך ההמתנה הרצוי לפני ניסיון Reconnect (במילישניות), מה שמאפשר שליטה בסיסית על קצב ה-Reconnection מבלי לממש שום קוד בצד הלקוח. שילוב נכון של השדות האלה — במיוחד `id` ו-`retry` — הוא ההבדל בין מימוש SSE "עובד באופן בסיסי" לכזה שמתמודד היטב עם תנאי רשת אמיתיים ומגוונים.
המגבלה המרכזית: חד-כיווניות
SSE זורם רק מהשרת ללקוח. אם הלקוח צריך לשלוח מידע חזרה — לא רק לקבל עדכונים אלא גם לפעול — הוא עושה זאת דרך בקשת HTTP רגילה נפרדת (POST רגיל), לא דרך אותו חיבור. עבור מקרים רבים זה בסדר גמור ואפילו רצוי — התקשורת ה"חוזרת" בכל מקרה לא בהכרח דורשת latency נמוך כמו הזרם החי עצמו. אבל עבור צ'אט, למשל, שבו שני הכיוונים דורשים latency נמוך וזרימה תכופה, הפרדת הכיוונים לשני מנגנוני תקשורת נפרדים (SSE יורד, HTTP POST עולה) מוסיפה מורכבות שלא הייתה קיימת עם WebSocket יחיד. ההכרעה בין השניים היא בעיקר שאלה של צורת התקשורת: אם מדובר בעדכון-שדר (feed חדשות, מחירי מניות, סטטוס משימה ארוכה) — SSE. אם מדובר בשיחה דו-כיוונית פעילה — WebSocket.
מגבלת חיבורים במקביל בדפדפן
נקודה טכנית שתופסת צוותים רבים לא מוכנים: דפדפנים מגבילים את מספר חיבורי HTTP/1.1 המקבילים לאותו domain (בדרך כלל 6). מכיוון ש-SSE הוא חיבור HTTP שנשאר פתוח, אם המשתמש פותח כמה טאבים של אותו אתר, כל טאב "תופס" אחד מששת החיבורים הזמינים, ובטאב השביעי SSE פשוט לא יתחבר עד ששאר החיבורים ישתחררו. הפתרון המודרני הוא HTTP/2, שבו כל חיבור TCP תומך בריבוי Streams במקביל ולכן המגבלה הזו כמעט נעלמת — אבל זה דורש שהשרת (ותשתית ה-Proxy שלפניו) יתמכו ב-HTTP/2 בפועל, לא רק שהלקוח יבקש אותו. לפני שמאמצים SSE בקנה מידה, כדאי לוודא שה-Load Balancer וה-Reverse Proxy בשרשרת (nginx, ALB, Cloudflare) מוגדרים לתמוך ב-HTTP/2 end-to-end ולא רק בין הדפדפן ל-Edge.
Proxies ו-Buffering: המלכודת השקטה
המגבלה המסוכנת ביותר בפרודקשן היא Buffering לא רצוי בשכבות ביניים. nginx, למשל, מבצע Buffering של תגובות ברירת מחדל — מה שאומר שה-nginx ממתין לצבור כמות מסוימת של נתונים לפני שהוא שולח אותם הלאה ללקוח, בדיוק ההיפך ממה ש-SSE צריך (שידור מיידי של כל אירוע ברגע שהוא נוצר). התוצאה: SSE שעובד מושלם בסביבת פיתוח (בלי Proxy) "נתקע" בפרודקשן — הלקוח לא מקבל עדכונים בזמן אמת אלא בפרצי buffer גדולים ומעוכבים. הפתרון ב-nginx הוא כותרת ייעודית שמכבה Buffering לנתיב הספציפי:
location /stream {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
זו לא הגדרה תיאורטית — זו הסיבה השכיחה ביותר לכך ש-"SSE לא עובד בפרודקשן" בדיווחי באגים אמיתיים, ולכן היא הדבר הראשון שכדאי לבדוק כשמשהו לא זורם כמצופה.
סקיילינג SSE: אותה בעיה כמו WebSocket, פתרון דומה
חשוב לזכור ש-SSE סובל מבעיית Scaling דומה ל-WebSocket — כל חיבור SSE פתוח עדיין "נצמד" לשרת שקיבל אותו, ואם צריך לשדר עדכון ללקוח ספציפי משרת אחר, נדרשת אותה שכבת Pub/Sub שתוארה עבור WebSocket. ההבדל המעשי היחיד הוא כיוון הזרימה — כאן זה תמיד מהשרת ללקוח, כך שאין צורך בניתוב הודעות שמגיעות מהלקוח פנימה, מה שמפשט מעט את הארכיטקטורה הכוללת. עדיין, כשמתכננים SSE בקנה מידה, כדאי להתייחס לזה כבעיית Fan-Out קלאסית — Redis Pub/Sub, או תור הודעות עם צריכה מקבילה, הם הפתרון הסטנדרטי גם כאן. שווה גם לשקול פתרונות מנוהלים ייעודיים (Pusher, Ably, AWS AppSync) כשהיקף התעבורה גדול מספיק כדי להצדיק תלות בספק חיצוני — הם פותרים את כל שכבת ה-Fan-Out וה-Scaling מראש, במחיר גמישות מסוימת ותלות בזמינות הספק.
ניהול Timeout ו-Load Balancer
Load Balancers רבים סוגרים חיבורי HTTP שנשארים פתוחים יותר מדי זמן ללא תעבורה (Idle Timeout ברירת מחדל של 60 שניות ב-ALB, למשל). כדי למנוע ניתוק "מיותר" של חיבור SSE שפעיל אבל שקט זמנית (אין עדכונים חדשים), חובה לשלוח Keep-Alive תקופתי — לרוב שורת comment (`: keep-alive`) כל 15-30 שניות, שמעדכנת את ה-Load Balancer שהחיבור עדיין חי בלי לשלוח אירוע עסקי אמיתי. גם כאן, Reconnection האוטומטי של EventSource הופך את הבעיה הזו לפחות קריטית ממה שהייתה עם WebSocket — גם אם Timeout קורה, הלקוח מתחבר מחדש בשקט וממשיך מאיפה שהפסיק.
שימוש ב-SSE עם Streaming של תשובות AI
אחד השימושים הנפוצים ביותר ל-SSE כיום, ורלוונטי במיוחד לצוותים שבונים מוצרי AI, הוא הזרמת תשובות ממודל שפה טוקן-אחר-טוקן — בדיוק החוויה שרואים בממשקי צ'אט כמו ChatGPT או Claude, שבה הטקסט "נכתב" בזמן אמת במקום להופיע כגוש שלם בסוף. זה מקרה שימוש שמתאים ל-SSE כמו כפפה ליד: התקשורת חד-כיוונית לחלוטין (מהמודל אל הלקוח), כל טוקן שמתקבל מהמודל נשלח כאירוע SSE נפרד ברגע שהוא זמין, וה-Reconnection המובנה מטפל בניתוקים חולפים בלי לאבד את כל התשובה שכבר נוצרה. רוב ה-SDKs של ספקי מודלי שפה (כולל Anthropic) תומכים ב-Streaming Response שמיועד בדיוק לעטיפה ב-SSE כלפי הלקוח הסופי, מה שהופך אותו לתשתית טבעית לכל שכבת Backend שמתווכת בין Frontend למודל AI.
מתי SSE ולא WebSocket
הכלל הפשוט: אם התקשורת חד-כיוונית מהשרת (feed, התראות, מדדים, לוג חי) — SSE כמעט תמיד עדיף, כי הוא פשוט יותר, עובד מעל תשתית HTTP קיימת, ומגיע עם Reconnection מובנה. אם התקשורת דו-כיוונית ותכופה משני הצדדים — WebSocket מתאים יותר. השילוב הנפוץ בפועל הוא SSE לזרם הודעות שדר (למשל, "רשימת המשימות שלך התעדכנה") ו-HTTP POST רגיל לפעולות שהלקוח יוזם — פשוט יותר מ-WebSocket דו-כיווני עבור מרבית אפליקציות ה-Dashboard וה-Notification.
אימות ו-CORS ב-SSE
מאחר ש-SSE מתבצע דרך `EventSource`, יש לו מגבלה שכדאי להכיר: הממשק הסטנדרטי בדפדפן לא תומך בשליחת כותרות מותאמות אישית (כמו `Authorization: Bearer ...`) בבקשה עצמה. הפתרון הנפוץ הוא אחד משלושה: להעביר את ה-Token כפרמטר Query String (פחות מומלץ מבחינת אבטחה כי הוא עלול להישמר בלוגים של שרתים בדרך), להסתמך על Cookie מאובטח (HttpOnly) שנשלח אוטומטית עם כל בקשה כולל SSE, או להשתמש בספריית Polyfill שמממשת את `EventSource` מעל `fetch` עם כותרות מלאות. ברוב המערכות בפרודקשן, הגישה הפרקטית ביותר היא Cookie מבוסס Session, בדיוק כמו שמשתמשים בו לאימות דפי HTML רגילים — פשוט, מאובטח כשמוגדר נכון (HttpOnly, Secure, SameSite), ולא דורש שינוי בממשק ה-EventSource הסטנדרטי.
סיכום
SSE הוא כלי שמפתחים רבים מדלגים עליו ישר ל-WebSocket, ומפספסים כלי שמתאים בדיוק לתרחיש הנפוץ ביותר בפועל — עדכונים חד-כיווניים מהשרת. ההבנה של מגבלות ה-Proxy Buffering, מגבלת החיבורים בדפדפן, וניהול Timeout נכון היא ההבדל בין SSE שעובד מושלם בפיתוח לכזה שקורס בשקט בפרודקשן.
תגיות: Server-Sent Events · SSE · Real-Time · HTTP Streaming · EventSource · Reconnection