SSE (Server-Sent Event) with techY
Normally, a web page requests data from the server, but with SSE( Server-Sent Event) server can send new data to the web page any time…
SSE (Server-Sent Event) with techY
Normally, a web page requests data from the server, but with SSE( Server-Sent Event) server can send new data to the web page any time without waiting for the browser to request it.
What is SSE?
SSE is a built in browser API that enables real-time, one way communication( WebSockets provides full duplex communication ) it from server to client over the HTTP.
Let’s take a simple example

How I use SSE in our platform “techY”
While developing techY, my technology-focused social networking platform, I wanted messages, typing indicators, and read receipts to update without requiring users to refresh the page.
Instead of using WebSockets, I implemented real-time communication using Server-Sent Events, commonly known as SSE.
This model suited techY because the browser sends actions through API routes, while the server only needs to push updates back to connected users.

The implementation consists of four main parts:
- An SSE endpoint that maintains browser connections.
- An in-memory registry of connected users.
- A publisher that sends events to selected users.
- A frontend hook that listens for events and updates the UI.
Creating the SSE Endpoint
The browser connects to this endpoint:
/api/users/messages/events
The route first authenticates the user and retrieves their internal MongoDB user ID. It then creates a **ReadableStream**:
const stream = new ReadableStream({
start(controller) {
const send = (event: MessageRealtimeEvent) => {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
};
send({ type: "connected", userId });
const unsubscribe = subscribeUser(userId, send);
},
});
The response requires SSE-specific headers:
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
},
});
Each event is formatted as:
data: {"type":"connected","userId":"123"}
The two newline characters are important because they indicate the end of an SSE event.
Keeping Connections Alive
Some servers and proxies close inactive HTTP connections. techY sends a heartbeat every 25 seconds:
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(": heartbeat\n\n"));
}, 25000);
Lines beginning with : are SSE comments. They keep the connection active without triggering a normal message event.
When the browser disconnects, the application clears the heartbeat and removes the subscription:
req.signal.addEventListener("abort", () => {
clearInterval(heartbeat);
unsubscribe();
controller.close();
});
Tracking Connected Users
techY maintains an in-memory map of user IDs and callbacks:
Map<string, Set<Subscriber>>
A **Set** is used because one user may open TechY in multiple tabs.
export function subscribeUser(userId: string, callback: Subscriber) {
if (!subscribers.has(userId)) {
subscribers.set(userId, new Set());
}
subscribers.get(userId)!.add(callback);
return () => {
subscribers.get(userId)?.delete(callback);
};
}
To send an event to a particular user:
export function publishToUser(userId: string, event: MessageRealtimeEvent) {
const connections = subscribers.get(userId);
connections?.forEach((callback) => {
callback(event);
});
}
Sending a Real-Time Message
Messages are still submitted through a normal POST request:
POST /api/users/messages
The server authenticates the sender, verifies conversation access, checks that both users mutually follow each other, and stores the message in MongoDB.
After saving it, the server creates a real-time event:
const event = {
type: "new_message",
conversationId: conversation._id.toString(),
message: populatedMessage,
};
The event is published to both participants:
publishToUser(senderId, event);
publishToUser(receiverId, event);
Publishing to the sender keeps multiple tabs synchronized. Publishing to the receiver delivers the message without polling or refreshing.
Listening in React
On the frontend, I created an EventSource connection:
const eventSource = new EventSource(
"/api/users/messages/events",
{ withCredentials: true }
);
Incoming events are parsed and processed:
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "new_message") {
handleNewMessage(data);
}
};
When a message arrives, techY updates its TanStack React Query cache and invalidates related queries:
queryClient.invalidateQueries({
queryKey: queryKeys.messages.conversations(),
});
queryClient.invalidateQueries({
queryKey: queryKeys.messages.unreadCount(),
});
If the incoming message belongs to the currently open conversation, it is added directly to the cached message list.
Automatic Reconnection
An SSE connection may fail because of network interruptions or server restarts. The frontend reconnects after 2.5 seconds:
eventSource.onerror = () => {
setConnected(false);
eventSource.close();
reconnectTimer = setTimeout(connect, 2500);
};
This makes the messaging interface recover automatically from temporary failures.
Typing Indicators
Typing updates use regular POST requests:
POST /api/users/messages/{conversationId}/typing
When typing begins, the client sends:
{ "isTyping": true }
After two seconds without input, it sends:
{ "isTyping": false }
The server publishes a typing event to the other participant:
publishToUser(otherUserId, {
type: "typing",
conversationId,
userId: currentUserId,
isTyping,
});
The receiver processes this event and displays the typing indicator only when the corresponding conversation is active.
Read Receipts
When a conversation is opened, the frontend sends:
PATCH /api/users/messages/{conversationId}/read
The server marks unread messages from the other participant as read and publishes:
{
type: "messages_read",
conversationId,
readBy: currentUserId
}
The original sender receives this event and updates their messages to show the read state.
Supported Events
TechY currently supports four SSE events:
type MessageRealtimeEvent =
| { type: "connected"; userId: string }
| { type: "new_message"; conversationId: string; message: object }
| { type: "typing"; conversationId: string; userId: string; isTyping: boolean }
| { type: "messages_read"; conversationId: string; readBy: string };
Using a discriminated union makes event handling predictable and type-safe.
Why I Chose SSE
SSE was suitable for techY because:
- It works over standard HTTP.
- Browsers provide the built-in
EventSourceAPI. - It is simpler than maintaining a WebSocket protocol.
- The browser automatically receives server updates.
- Message submission can continue using ordinary REST APIs.
- It supports heartbeats and reconnection.
- It integrates cleanly with Next.js streaming responses.
Current Limitation
techY currently stores subscribers in server memory. This works during local development and on a single persistent Node.js instance.
However, if the application runs across several instances, a user may connect to one instance while a message is processed by another. The second instance would not know about the first instance’s subscriber map.
A production-scale version should use a shared event layer such as:
- Redis Pub/Sub
- Ably
- Pusher
- Apache Kafka
- Socket.IO with a Redis adapter
The API instance would publish events to the shared service, and every application instance could forward them to its connected browsers.
Conclusion
SSE gave techY a straightforward way to implement real-time messaging without introducing a complete WebSocket infrastructure.
Regular HTTP requests handle client actions, MongoDB provides persistent storage, and SSE pushes resulting events back to connected users. This architecture now supports new messages, typing indicators, read receipts, multiple browser tabs, heartbeats, and automatic reconnection.
Building this feature helped me understand that “real-time” communication is not only about transferring data quickly. It also requires authentication, connection lifecycle management, database consistency, cache synchronization, access control, failure recovery, and a clear event model.
That combination made SSE an effective and educational choice for techY.

메타데이터
- post_id
- fae1cd3fd183
- slug
- sse-server-sent-event-with-techy-fae1cd3fd183
- url
- https://medium.com/@prmjtsaikia/sse-server-sent-event-with-techy-fae1cd3fd183
- canonical_url
- https://medium.com/@prmjtsaikia/sse-server-sent-event-with-techy-fae1cd3fd183
- author_url
- https://medium.com/@prmjtsaikia
- status
- ok
- fetched_at
- 2026-06-13 12:55:53