Server-Sent Events vs WebSockets untuk Realtime

Foto oleh Blackcat via Wikimedia Commons (CC BY-SA 4.0)
Server-Sent Events bersifat satu arah: server men-streaming data ke klien lewat request HTTP berumur panjang, dan klien tidak bisa mengirim data balik lewat kanal yang sama. WebSockets bersifat dua arah dan full-duplex, sehingga klien dan server bisa mendorong pesan kapan saja. Arah aliran data itulah faktor utama dalam memilih di antara keduanya.
Ya. API EventSource di browser reconnect otomatis saat koneksi putus, dengan interval retry default sekitar tiga detik yang bisa ditimpa server lewat field retry. Jika setiap event membawa id, browser mengirim header Last-Event-ID saat reconnect sehingga server bisa melanjutkan dari titik berhenti. WebSockets tidak punya reconnection bawaan, jadi Anda harus menulis logika itu sendiri.
Hanya di HTTP/1.1. Browser membatasi koneksi bersamaan per origin sekitar enam, dan setiap stream SSE memakan satu, yang bisa membuat halaman dengan banyak tab kelaparan. HTTP/2 mem-multiplex banyak stream di atas satu koneksi TCP, dengan default hasil negosiasi sekitar seratus, sehingga batasan itu praktis hilang di deployment HTTP/2 modern.
Penyebab umumnya adalah buffering response. Nginx mem-buffer response yang di-proxy secara default, menahan event sampai buffer penuh alih-alih mem-flush satu per satu segera. Menyetel proxy_buffering off, menaikkan proxy_read_timeout, dan menambah header X-Accel-Buffering no memulihkan pengiriman real-time. WebSockets sebaliknya butuh proxy meneruskan handshake HTTP Upgrade.
Server-Sent Events adalah pilihan umum untuk streaming token AI atau LLM karena alirannya murni server ke klien dan ter-multiplex rapi di HTTP/2. Sebagian besar SDK AI besar default ke SSE justru karena alasan ini. Gunakan WebSockets hanya jika klien juga perlu men-streaming input terus-menerus balik ke server selama generasi.

Foto oleh Blackcat via Wikimedia Commons (CC BY-SA 4.0)
Ringkasan Utama
Pakai Server-Sent Events ketika server hanya perlu mendorong data ke klien: berjalan di atas HTTP biasa, reconnect otomatis, dan ter-multiplex rapi di HTTP/2. Gunakan WebSockets saat klien juga harus mengirim aliran data balik, seperti chat, editing kolaboratif, atau game, di mana aliran dua arah penuh sepadan dengan protokol tambahannya.
Setiap beberapa bulan sebuah proyek meminta fitur realtime dan seseorang langsung meraih WebSockets. Sering kali itu berlebihan. Sebagian besar fitur realtime bersifat satu arah: dashboard yang diperbarui, progress bar untuk job panjang, notifikasi, live log, atau model AI yang men-streaming token. Untuk semua itu, Server-Sent Events menyelesaikan tugas dengan kode jauh lebih sedikit dan beban operasional jauh lebih ringan.
Saya menjalankan beberapa layanan di VPS self-hosted di belakang reverse proxy, dan pilihan antara dua transport ini punya konsekuensi nyata terhadap berapa banyak logika reconnection yang harus saya tulis, bagaimana proxy berperilaku, dan berapa banyak koneksi yang bahkan diizinkan oleh browser. Berikut cara saya menalarnya dalam praktik, dengan trade-off yang benar-benar penting.
Server-Sent Events bersifat satu arah secara desain. Klien membuka satu request HTTP berumur panjang dan server men-streaming aliran teks event ke bawahnya, terus-menerus, sampai ada yang menutupnya. Klien tidak bisa mengirim data balik lewat kanal yang sama; untuk berbicara ke server ia membuat request HTTP biasa di samping stream itu. Sebaliknya, WebSockets meng-upgrade koneksi menjadi protokol full-duplex di mana kedua sisi mendorong frame kapan pun mereka mau.
Perbedaan tunggal itu menentukan sebagian besar kasus. Jika interaksinya benar-benar percakapan, dengan klien memancarkan pesan terus-menerus, WebSockets cocok secara alami. Jika klien sebagian besar hanya pendengar yang sesekali membuat request biasa, SSE adalah model yang lebih sederhana dan lebih jujur. Memaksakan pipa dua arah pada masalah satu arah hanya menambah permukaan yang harus diamankan dan dipelihara.
Ini adalah fitur SSE yang paling diremehkan orang. API EventSource di browser reconnect otomatis saat koneksi putus, dengan retry default sekitar tiga detik yang bisa diubah server dengan mengirim field retry di dalam stream. Lebih bagus lagi, jika Anda melampirkan id pada setiap event, browser menyimpannya dan mengirim header Last-Event-ID saat reconnect, sehingga server bisa melanjutkan tepat dari titik terakhir alih-alih memutar ulang atau menjatuhkan pesan.
// SSE stream — reconnection and resume are built in
// Client:
const es = new EventSource("/api/updates");
es.onmessage = (e) => console.log("data:", e.data);
es.onerror = () => console.log("reconnecting automatically...");
// Server (Node/Express) response body format:
// id: 42\n
// retry: 5000\n
// data: {"progress":73}\n\n
// On reconnect the browser sends: Last-Event-ID: 42
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
res.write(`id: ${seq}\nretry: 5000\ndata: ${JSON.stringify(payload)}\n\n`);WebSockets tidak punya semua ini. Saat WebSocket tertutup, klien hanya diam; Anda harus menulis sendiri loop reconnect, backoff, dan logika replay pesan, atau memakai library yang melakukannya. Bagi banyak tim biaya tersembunyi itu menjadi faktor penentu: SSE menghadirkan perilaku tangguh yang jika tidak, akan Anda implementasikan ulang, dengan buruk, di bawah tenggat waktu.
Karena SSE hanyalah response HTTP panjang, ia biasanya lewat melalui infrastruktur yang ada, load balancer, middleware auth, dan CDN, tanpa perlakuan khusus. WebSockets butuh handshake HTTP Upgrade eksplisit, dan setiap proxy di jalur harus dikonfigurasi untuk mengizinkan dan meneruskannya. Itu lebih banyak bagian bergerak yang harus dibereskan.
SSE punya satu jebakan klasik di belakang reverse proxy: buffering response. Nginx mem-buffer response yang di-proxy secara default, jadi ia menahan event Anda sampai buffer penuh alih-alih mem-flush satu per satu. Anda harus menonaktifkan buffering dan menaikkan read timeout, atau stream realtime Anda tiba dalam batch yang senyap.
# Nginx: SSE endpoint behind a reverse proxy
location /api/updates {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Connection ""; # keep upstream alive
proxy_buffering off; # critical: flush each event
proxy_cache off;
proxy_read_timeout 86400s; # do not kill idle-ish streams
add_header X-Accel-Buffering no; # override buffering explicitly
}
# WebSocket endpoint needs the Upgrade dance instead:
location /ws {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Keluhan lama terhadap SSE adalah batas koneksi HTTP/1.1: browser hanya mengizinkan sekitar enam koneksi bersamaan per origin, dan setiap stream SSE memakan satu, yang bisa membuat sisa halaman kelaparan di beberapa tab. HTTP/2 menghapus batasan itu dengan mem-multiplex banyak stream di atas satu koneksi TCP, dengan default hasil negosiasi sekitar seratus stream bersamaan. Jadi di deployment HTTP/2 modern mana pun, batasan itu praktis hilang, yang menjadi alasan besar SSE menjadi transport default bagi SDK AI yang men-streaming token model.
| Dimensi | Server-Sent Events | WebSockets |
|---|---|---|
| Arah | Server ke klien saja | Dua arah penuh |
| Protokol | HTTP biasa, text/event-stream | Protokol terpisah lewat HTTP Upgrade |
| Reconnection | Otomatis, dengan resume Last-Event-ID | Manual, Anda bangun backoff dan replay |
| Keramahan proxy | Lewat sebagai HTTP biasa, awasi buffering | Butuh konfigurasi Upgrade di tiap hop |
| Perilaku HTTP/2 | Multiplex, batas per-origin hilang | Tanpa multiplexing bawaan, TCP sendiri |
| Payload data | Hanya teks UTF-8 | Frame teks dan biner |
Hibrida yang berguna: streaming update server lewat SSE dan kirim aksi klien sesekali lewat HTTP POST biasa. Anda mendapat push plus reconnection gratis sambil menjaga penanganan request di HTTP yang familiar, bisa di-cache, dan ramah auth. Naik ke WebSockets hanya saat obrolan klien-ke-server menjadi konstan.
Kesalahan yang paling sering saya lihat adalah meraih alat yang lebih powerful secara refleks. WebSockets memang lebih baik saat Anda membutuhkannya, tapi ia membawa biaya berkelanjutan: kode reconnection, konfigurasi proxy, dan protokol yang berada di luar tooling HTTP normal Anda. SSE diam-diam menyelesaikan kasus umum. Cocokkan transport dengan arah data Anda yang sebenarnya, dan sebagian besar fitur realtime jadi lebih sederhana.