TEKNİK YAZI / Backend Sistemleri

Server-Sent Events (SSE): Basit Gerçek Zamanlı Veri Akışı

Server-Sent Events (SSE) ile sunucudan tarayıcıya tek yönlü akış: olay biçimi, yeniden bağlanma, Last-Event-ID, proxy tamponlama ve WebSocket karşılaştırması.

Server-Sent Events (SSE), sunucunun tek bir uzun ömürlü HTTP yanıtı üzerinden tarayıcıya olay akışı göndermesini sağlayan web standardıdır. Tarayıcıdaki EventSource API’si bağlantıyı açar, gelen olayları ayrıştırır ve bağlantı koparsa yeniden kurar. Yalnız sunucudan istemciye veri akışı gereken durumlarda WebSocket’ten daha sade bir seçenektir ve mevcut HTTP altyapısıyla doğrudan çalışır.

Aşağıda olay akışının biçimi, Node.js ile bir sunucu ve tarayıcı istemcisi, Last-Event-ID ile kaldığı yerden devam etme, proxy tamponlama ve kimlik doğrulama tuzakları ile SSE’nin WebSocket’e göre ne zaman tercih edileceği ele alınıyor.

SSE hangi sorunu çözer?

Klasik istek-yanıt modelinde istemci yeni veri olup olmadığını öğrenmek için sunucuya sık sık sorar (polling). Bildirim, ilerleme durumu veya canlı ölçüm gibi tek yönlü akışlarda bu tekrar eden istekler hem gecikme hem gereksiz yük üretir.

SSE’de istemci tek bir istek açar, sunucu Content-Type: text/event-stream ile yanıt verir ve yanıtı kapatmadan yeni olayları yazmaya devam eder. Bağlantı koparsa tarayıcı belirli bir süre bekleyip yeniden bağlanır ve aldığı son olayın kimliğini sunucuya bildirir. Kaldığı yerden devam etmek için sunucunun olayları kimlikleriyle saklaması gerekir.

SSE, WebSocket ve long polling

Long polling: İstemci istek açar, sunucu veri oluşana ya da zaman aşımına (timeout) kadar yanıtı bekletir; yanıttan sonra yeni istek gerekir. Bağlantı sürelerinin sınırlandığı ortamlarda hâlâ geçerli bir uyumluluk seçeneğidir.

WebSocket: İki yönlü ve eş zamanlı (full-duplex) mesajlaşma sağlar. İstemciden sunucuya da sürekli olay akan ortak düzenleme, çok oyunculu oyun veya sohbet gibi senaryolarda daha uygundur; bağlantı durumu ve geri basınç (backpressure) yönetimi uygulamaya kalır.

SSE: Sunucudan istemciye tek yönlüdür, düz HTTP üzerinde çalışır ve tarayıcının yerleşik EventSource API’sini kullanır. Otomatik yeniden bağlanma ve olay kimliği standarda dahildir; buna karşın kimlik doğrulama, proxy tamponlama ve açık bağlantı sınırları ayrıca ele alınmalıdır.

Ne zaman SSE kullanılır?

İstemcinin sunucuya yalnız arada bir veri göndermesi gerekiyorsa SSE ile normal HTTP istekleri birlikte kullanılabilir: akış SSE’den gelir, kullanıcı eylemleri ayrı POST istekleriyle gider. İstemciden sunucuya da sık ve düşük gecikmeli mesaj gerekiyorsa WebSocket daha doğru seçimdir.

Olay akışının biçimi

Akış UTF-8 kodlu düz metindir. Her olay bir veya daha fazla alan: değer satırından oluşur ve boş bir satırla biter:

: iki nokta ile başlayan satır yorumdur, heartbeat için kullanılır
retry: 5000

id: 41
data: Sipariş alındı

id: 42
event: notification
data: {"title": "Yeni mesaj"}

id: 43
data: birinci satır
data: ikinci satır

Alan adları büyük-küçük harfe duyarlıdır, tanınmayan alanlar yok sayılır. JSON gibi tek satırlık veriler doğrudan data: satırına yazılabilir; satır sonu içeren metin birden çok data: satırına bölünmelidir.

Node.js ile sunucu örneği

Aşağıdaki Express sunucusu her saniye bir olay yayımlar, olaylara kimlik verir, yeniden bağlanan istemciye kaçırdığı olayları gönderir ve 15 saniyede bir heartbeat yazar.

// server.js: Basitleştirilmiş örnek. Olay geçmişi bellekte tutulur;
// gerçek sistemde kalıcı bir depo veya mesaj aracısı kullanılır.
const express = require("express");

const app = express();
const clients = new Set();
const history = [];
let lastId = 0;

function send(res, event) {
  res.write(`id: ${event.id}\ndata: ${event.data}\n\n`);
}

function publish(payload) {
  const event = { id: ++lastId, data: JSON.stringify(payload) };
  history.push(event);
  if (history.length > 1000) history.shift(); // son 1000 olayı tut
  for (const res of clients) send(res, event);
}

app.get("/events", (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    "X-Accel-Buffering": "no", // nginx bu yanıtı tamponlamasın
  });
  res.write("retry: 5000\n\n"); // kopmadan sonra 5 saniye bekle

  // Yeniden bağlanan tarayıcı son aldığı olay kimliğini bu başlıkla gönderir
  const lastEventId = req.get("Last-Event-ID");
  if (lastEventId !== undefined) {
    const since = Number(lastEventId);
    for (const event of history) {
      if (event.id > since) send(res, event);
    }
  }

  clients.add(res);
  // Boşta kalan bağlantıyı proxy'lerin kapatmaması için yorum satırı yaz
  const heartbeat = setInterval(() => res.write(": ping\n\n"), 15000);

  // İstemci ayrıldığında zamanlayıcıyı ve kaydı temizle
  res.on("close", () => {
    clearInterval(heartbeat);
    clients.delete(res);
  });
});

setInterval(() => publish({ time: new Date().toISOString() }), 1000);

app.listen(3000);

Ayrılan istemciyi tespit etmek için res.on("close") kullanılır. Node.js 16’dan beri istek nesnesindeki close olayı alttaki bağlantı kapandığında değil, istek tamamlandığında tetiklenir; bu yüzden req.on("close") uzun ömürlü akışlarda temizliği yanlış zamanda çalıştırabilir.

Örnekteki olay geçmişi ve sayaç bellekte durduğu için sunucu yeniden başlayınca kimlikler sıfırlanır. Gerçek sistemde kimlikler ve kısa süreli olay geçmişi kalıcı bir depodan ya da mesaj aracısından gelmelidir.

Tarayıcı tarafı:

const source = new EventSource("/events");

// event alanı olmayan olaylar
source.onmessage = event => {
  console.log("Olay:", event.lastEventId, event.data);
};

// event: notification olarak gönderilen olaylar
source.addEventListener("notification", event => {
  const payload = JSON.parse(event.data);
  console.log("Bildirim:", payload.title);
});

source.onerror = () => {
  if (source.readyState === EventSource.CLOSED) {
    // Kalıcı kapanma: tarayıcı yeniden denemeyecek
    console.error("Akış kapandı");
  } else {
    // CONNECTING: tarayıcı kendisi yeniden bağlanıyor
    console.warn("Bağlantı koptu, yeniden deneniyor");
  }
};

Yeniden bağlanma ve kalıcı kapanma

Ağ hatasında veya sunucu bağlantıyı kapattığında EventSource en son retry değeri kadar bekler ve aynı adrese yeniden bağlanır. Son olay kimliği Last-Event-ID başlığıyla gönderildiği için sunucu kaçırılan olayları tekrar yollayabilir. Tarayıcı bu yeniden bağlanmayı kendisi yapar; uygulamanın ayrıca kod yazması gerekmez.

Bazı durumlarda ise bağlantı kalıcı olarak kapanır ve tarayıcı yeniden denemez:

Bu durumda readyState değeri CLOSED olur. Kimlik doğrulama hatasından sonra akışı sürdürmek isteyen uygulama yeni kimlik bilgileriyle yeni bir EventSource oluşturmalıdır.

Proxy, sıkıştırma ve bağlantı sınırları

SSE, yanıtın parça parça istemciye ulaşmasına dayanır. Araya giren her katman yanıtı tamponlarsa olaylar gecikir ya da hiç gelmez.

Aynı tamponlama sorunu server-side rendering’deki streaming yanıtlarında da görülür; her iki durumda da proxy ve sıkıştırma ayarları birlikte kontrol edilmelidir.

Ölçekleme

Her SSE bağlantısı, istemci ayrılana kadar bir sunucu örneğine bağlı kalır. Birden fazla sunucu örneği çalıştığında bir olayın tüm istemcilere ulaşması için olaylar ortak bir kanaldan dağıtılmalıdır: Redis pub/sub veya RabbitMQ gibi bir mesaj aracısı her örneğe olayın bir kopyasını iletir, örnek de bunu kendi bağlantılarına yazar.

Yeniden bağlanan istemci farklı bir örneğe düşebilir. Last-Event-ID ile kaçırılan olayları gönderebilmek için olay geçmişi ve kimlikler örnekler arasında paylaşılan bir yerde tutulmalıdır.

Kimlik doğrulama ve güvenlik

EventSource yalnız GET isteği açar ve Authorization gibi özel başlık eklemeye izin vermez. Bu yüzden kimlik doğrulama için şu seçenekler kalır:

Bunlara ek olarak akışı yalnız HTTPS üzerinden sunun, kullanıcı başına açık bağlantı sayısını ve bağlantı açma hızını sınırlayın.

SSE’nin sınırları

Gerçek dünyada kullanım

Büyük dil modeli API’leri SSE’nin bugün en görünür kullanımlarından biridir. Anthropic’in Messages API’si "stream": true verildiğinde yanıtı SSE olarak gönderir; olaylar message_start, content_block_delta gibi event: adları taşır ve araya ping olayları girer. OpenAI’nin akış modu da SSE kullanır. Bu API’ler isteği POST ile açtığı için tarayıcıda EventSource yerine fetch ile okunur ya da sağlayıcının SDK’sı kullanılır. Bu akışı kendi backend’iniz üzerinden aktarırken kullanımı ve maliyeti doğru ölçmek ayrı bir sorundur; LLM API gateway yazısı olay sırasını, kümülatif kullanım alanlarını ve yarım kalan akışların faturalanmasını ele alıyor.

SSE mi WebSocket mi?

SSE seçin:

WebSocket seçin:

Diğer seçenekler

WebTransport: HTTP/3 üzerinde çalışır; tek bağlantıda çok sayıda akış ve güvenilmez (UDP benzeri) datagram sunar. MDN’e göre Mart 2026’dan beri güncel ana tarayıcılarda desteklenir (Baseline 2026); bazı alt özelliklerin desteği farklılık gösterebilir ve sunucu tarafında HTTP/3 gerekir.

gRPC-Web: Tarayıcıdan gRPC servislerine erişim sağlar. Unary çağrıları ve yalnız grpcwebtext modunda sunucu akışını destekler; istemci ve çift yönlü akış desteklenmez. Araya Envoy gibi bir proxy gerekir.

GraphQL abonelikleri: GraphQL bir taşıma protokolü tanımlamaz; abonelikler WebSocket veya SSE üzerinden çalışan kütüphanelerle uygulanır.

Sonuç

SSE, sunucudan istemciye tek yönlü veri akışı gereken bildirim, ilerleme durumu, canlı pano ve model yanıtı akışı gibi senaryolarda az kodla güvenilir bir çözüm sunar. Olay kimliği, heartbeat ve tamponlamasız proxy ayarı olmadan bu güvenilirlik kâğıt üzerinde kalır. İstemciden sunucuya da yoğun mesaj gerekiyorsa ya da ikili veri taşınacaksa WebSocket veya WebTransport daha uygundur.

Birincil kaynaklar