Membangun komunikasi realtime dengan FastAPI: WebSocket endpoint dan ConnectionManager untuk chat, penyiaran pesan ke banyak klien, serta Server-Sent Events untuk notifikasi dan streaming data satu arah.

Setelah di episode 12 API kalian bisa mengenali pemakainya lewat JWT, sekarang kita buka dimensi baru: realtime. HTTP bekerja dengan pola request-response — klien harus bertanya untuk mendapatkan jawaban. WebSocket dan SSE membaliknya: server bisa mengirim data kapan pun, tanpa ditanya dulu.
Mengapa episode ini penting? Karena fitur-fitur yang membuat aplikasi terasa hidup — chat, notifikasi, update harga, progress bar — semuanya dibangun di atas pola ini. Dan karena FastAPI berbasis ASGI (episode 2), semua ini didukung native tanpa hack.
| HTTP | WebSocket | SSE | |
|---|---|---|---|
| Arah | Request-response | Bidirectional | Server → klien |
| Protokol | HTTP | Upgrade ke WebSocket | HTTP streaming |
| Kapan dipakai | API biasa | Chat, game, kolaborasi | Notifikasi, feed |
| Bawaan browser | Ya | Ya | Ya (EventSource) |
Aturan praktis: butuh dua arah (klien mengirim dan menerima terus) → WebSocket. Hanya server → klien (misal notifikasi) → SSE jauh lebih sederhana dan otomatis reconnect.
WebSocket di FastAPI dideklarasikan seperti path operation, tapi dengan @app.websocket:
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws/echo")
async def websocket_echo(websocket: WebSocket) -> None:
await websocket.accept()
while True:
message = await websocket.receive_text()
await websocket.send_text(f"echo: {message}")Alur wajib di setiap WebSocket handler:
await websocket.accept() — terima handshake.await websocket.receive_text() menunggu pesan masuk.await websocket.send_text(...) mengirim balik.Karena handler ini async def, satu event loop bisa melayani banyak koneksi WebSocket sekaligus — di sinilah keunggulan ASGI terasa.
Important
Jangan menulis loop while True dengan operasi blocking di dalamnya (seperti time.sleep). Blokir satu koneksi berarti memblokir seluruh event loop dan semua klien lain. Untuk menunda kirim gunakan await asyncio.sleep(...), bukan time.sleep.
Aplikasi realtime nyata butuh melacak semua koneksi dan mengirim pesan ke semuanya (broadcast). Ini pola ConnectionManager yang dipakai di hampir semua aplikasi FastAPI:
from fastapi import WebSocket
class ConnectionManager:
def __init__(self) -> None:
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket) -> None:
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket) -> None:
self.active_connections.remove(websocket)
async def broadcast(self, message: str) -> None:
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()active_connections adalah daftar semua WebSocket yang terhubung. broadcast mengirim pesan ke seluruhnya — fondasi untuk fitur chat dan notifikasi massal.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from app.ws import manager
app = FastAPI()
@app.websocket("/ws/chat")
async def websocket_chat(websocket: WebSocket) -> None:
await manager.connect(websocket)
try:
while True:
message = await websocket.receive_text()
await manager.broadcast(f"pesan: {message}")
except WebSocketDisconnect:
manager.disconnect(websocket)Kunci penting: WebSocketDisconnect di-catch untuk membersihkan koneksi dari daftar. Tanpa ini, koneksi yang putus tetap "berada" di active_connections, dan broadcast akan melempar error saat mengirim ke koneksi mati.
FastAPI menyediakan TestClient untuk WebSocket juga:
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_websocket_echo() -> None:
with client.websocket_connect("/ws/echo") as ws:
ws.send_text("halo")
data = ws.receive_text()
assert data == "echo: halo"Seluruh logika WebSocket bisa diuji tanpa membuka server — konsisten dengan pola testing episode 10.
Untuk notifikasi satu arah, SSE lebih ringan daripada WebSocket. Klien cukup membuka EventSource di browser, dan server streaming event sebagai text:
import asyncio
import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
async def event_stream():
for i in range(5):
yield f"data: {json.dumps({'counter': i})}\n\n"
await asyncio.sleep(1)
@app.get("/events")
async def sse_events() -> StreamingResponse:
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache"},
)Format SSE mengharuskan baris data: ... diakhiri dua baris kosong. Media type wajib text/event-stream — browser EventSource mengenalinya.
Tip
Memilih WebSocket vs SSE: untuk progress upload, feed berita, atau notifikasi — SSE cukup (dan auto-reconnect gratis via EventSource). Untuk chat, kolaborasi, atau game — WebSocket karena butuh dua arah. Mulai yang paling sederhana yang bisa menyelesaikan masalah.
WebSocket tidak mengirim header Authorization dari browser, jadi token biasanya dikirim lewat query param:
from fastapi import WebSocket, WebSocketException, status
@app.websocket("/ws/secure")
async def websocket_secure(
websocket: WebSocket, token: str
) -> None:
if token != "secret-token":
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
await websocket.accept()WS_1008_POLICY_VIOLATION adalah code standar untuk penolakan karena kebijakan. Pertukaran token bisa dienkapsulasi sebagai dependency WebSocket — pola yang sama dengan Depends biasa.
Warning
Memvalidasi token lewat query string punya risiko log (token ikut tercatat di access log). Di produksi, pertimbangkan mekanisme yang lebih aman seperti cookie WebSocket atau short-lived ticket, dan pastikan TLS aktif (wss://) agar token tidak terbaca di jaringan.
| Pitfall | Solusi |
|---|---|
Lupa await websocket.accept() | Koneksi menunggu selamanya |
Tidak catch WebSocketDisconnect | Koneksi mati tetap di daftar, broadcast error |
time.sleep di async handler | Gunakan await asyncio.sleep |
| SSE media type salah | Klien EventSource tidak mau konek |
| Header auth di WebSocket browser | Kirim token via query param / cookie |
Inti yang harus dibawa pulang:
ConnectionManager melacak semua koneksi dan mendukung broadcast.accept() dulu, lalu loop receive/send, dan catch WebSocketDisconnect.StreamingResponse dengan text/event-stream dan format data: ....Di episode 14 selanjutnya kita akan membahas background tasks & async database — menjalankan pekerjaan berat setelah response dikirim, mengombinasikan FastAPI dengan SQLAlchemy async dan asyncpg, serta memahami concurrency di dunia async. API kalian akan semakin cepat dan tangguh!