# SSE vs Streamable HTTP in MCP: Key Differences and Implementation Guide

> Understand SSE vs Streamable HTTP in MCP. Discover key differences in streaming, endpoints, and session management. Implement MCP efficiently with this guide.

- Repository: [Model Context Protocol/servers](https://github.com/modelcontextprotocol/servers)
- Tags: deep-dive
- Published: 2026-03-01

---

**While both transports use HTTP, SSE uses separate GET `/sse` and POST `/message` endpoints with unidirectional streaming and no message resumability, whereas Streamable HTTP offers a unified `/mcp` endpoint with bidirectional streaming, client-managed sessions via the `mcp-session-id` header, and automatic event replay through an in-memory event store.**

The Model Context Protocol (MCP) provides flexible transport abstractions for connecting AI clients and servers. In the `modelcontextprotocol/servers` repository, developers can choose between **Server-Sent Events (SSE)** and **Streamable HTTP** transports depending on reliability requirements and session management needs. Understanding the difference between SSE and streamable HTTP transports in MCP helps you select the appropriate implementation for stateless updates versus long-running, resumable interactions.

## Connection Model and Endpoint Structure

The fundamental architectural divergence begins with how each transport handles HTTP endpoints.

**SSE Transport (`SSEServerTransport`)** splits communication across two distinct routes. According to the source code in [`src/everything/transports/sse.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/transports/sse.ts) (lines 25-48), the server creates a new transport instance when a client issues a **GET request to `/sse`**, which establishes a persistent connection for server-to-client streaming. Client-to-server messages must be sent separately via **POST requests to `/message`** (lines 58-71), requiring the client to include the `sessionId` as a query parameter.

**Streamable HTTP Transport (`StreamableHTTPServerTransport`)** consolidates all communication into a single **`/mcp` endpoint** that handles POST, GET, and DELETE methods. As implemented in [`src/everything/transports/streamableHttp.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/transports/streamableHttp.ts), this design enables true bidirectional communication over one connection: clients send JSON-RPC requests via POST and receive streamed responses on the same transport, while GET requests allow clients to subscribe to incremental server events.

## Session Management and Identification

Session handling represents a critical operational difference between the two transports.

**SSE** generates the session identifier automatically during the initial GET request. The server stores the transport instance in a server-side map keyed by `transport.sessionId`, returning this ID to the client through the SSE stream itself. The client must capture this ID from the first event message to use in subsequent POST requests to the `/message` endpoint.

**Streamable HTTP** shifts session ownership to the client through the **`mcp-session-id` HTTP header**. When a client makes an initial POST request without this header (lines 59-84 in [`streamableHttp.ts`](https://github.com/modelcontextprotocol/servers/blob/main/streamableHttp.ts)), the server generates a new session ID and returns it in the response header. Clients include this header on all subsequent requests to maintain state. Explicit session termination occurs via DELETE requests (lines 64-98), which trigger cleanup hooks distinct from simple connection closure.

## Resumability and Event Replay

The transports differ significantly in handling connection drops and message recovery.

The **SSE transport provides no built-in event store**—if the connection drops, the client must establish a fresh stream and loses any events transmitted during the outage. This makes SSE suitable for "fire-and-forget" scenarios like live UI updates where historical message loss is acceptable.

The **Streamable HTTP transport implements an `InMemoryEventStore`** (lines 11-36 in [`streamableHttp.ts`](https://github.com/modelcontextprotocol/servers/blob/main/streamableHttp.ts)) that assigns a monotonic `eventId` to every dispatched message. When resuming a connection, clients send the **`Last-Event-ID` header** with the ID of the last received message. The server then replays all events occurring after that ID from the in-memory store, ensuring reliable delivery for long-running tool calls and stateful interactions.

## Lifecycle Hooks and Connection Management

Both transports register cleanup handlers, but Streamable HTTP offers more granular control.

In **[`src/everything/transports/sse.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/transports/sse.ts)**, the transport removes itself from the active sessions map when the underlying SSE connection closes via `server.server.onclose` (lines 25-48). This passive cleanup works well for simple request-response cycles but offers no explicit termination mechanism.

The **Streamable HTTP** implementation in [`streamableHttp.ts`](https://github.com/modelcontextprotocol/servers/blob/main/streamableHttp.ts) registers both `onsessioninitialized` (lines 78-84) for setup and `server.server.onclose` (lines 87-96) for teardown. Additionally, the DELETE method provides explicit session termination, allowing clients or administrators to forcibly close sessions and free resources without waiting for connection timeout.

## Implementation Examples

### SSE Client Implementation

When using the SSE transport, clients must manage two separate connections and extract the session ID from the initial event:

```typescript
// Subscribe to an SSE stream
const evtSource = new EventSource('http://localhost:3001/sse');

// The server sends a sessionId in the first event; store it for later POSTs
let sessionId: string | undefined;
evtSource.onmessage = (e) => {
  if (!sessionId) {
    // First message contains the sessionId (transport.sessionId)
    sessionId = e.data;
  }
  console.log('Server event:', e.data);
};

// Send a client request over the complementary POST endpoint
async function sendMessage(payload: any) {
  if (!sessionId) throw new Error('No session yet');
  const resp = await fetch(`http://localhost:3001/message?sessionId=${sessionId}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
  return resp.json();
}

```

### Streamable HTTP Client Implementation

The Streamable HTTP transport simplifies client logic by using headers for session management and enabling connection resumption:

```typescript
// Helper to start or resume a session
async function startSession(): Promise<string> {
  const resp = await fetch('http://localhost:3001/mcp', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', id: 1 }),
  });
  const sessionId = resp.headers.get('mcp-session-id');
  if (!sessionId) throw new Error('No session ID returned');
  return sessionId;
}

// Send a request within an existing session
async function sendRequest(sessionId: string, request: object) {
  const resp = await fetch('http://localhost:3001/mcp', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'mcp-session-id': sessionId,
    },
    body: JSON.stringify(request),
  });
  return resp.json();
}

// Resume a lost connection, replaying missed events
async function resume(sessionId: string, lastEventId: string) {
  const resp = await fetch('http://localhost:3001/mcp', {
    method: 'GET',
    headers: {
      'mcp-session-id': sessionId,
      'Last-Event-ID': lastEventId,
    },
  });
  // The response body streams events that occurred after `lastEventId`
}

```

## Summary

- **Endpoint Structure**: SSE requires separate GET `/sse` and POST `/message` endpoints, while Streamable HTTP uses a unified `/mcp` endpoint supporting POST, GET, and DELETE methods.

- **Session Management**: SSE generates session IDs server-side during the initial connection, whereas Streamable HTTP uses the `mcp-session-id` header managed by the client.

- **Resumability**: SSE offers no message replay capability; Streamable HTTP provides an `InMemoryEventStore` with automatic event replay when clients specify `Last-Event-ID`.

- **Communication Direction**: SSE is unidirectional from server to client (with client messages sent separately), while Streamable HTTP supports bidirectional streaming over a single connection.

- **Lifecycle Control**: Streamable HTTP supports explicit session termination via DELETE requests, while SSE relies solely on connection closure detection.

## Frequently Asked Questions

### Can I switch from SSE to Streamable HTTP without changing my MCP server logic?

Yes, the transport layer is abstracted from the core MCP protocol implementation. Both `SSEServerTransport` and `StreamableHTTPServerTransport` in `modelcontextprotocol/servers` wrap the same `McpServer` instance from [`src/everything/server/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/server/index.ts), allowing you to swap transports without modifying tool definitions or request handlers.

### How does Streamable HTTP handle session resumption after a network failure?

The transport maintains an `InMemoryEventStore` (lines 11-36 of [`streamableHttp.ts`](https://github.com/modelcontextprotocol/servers/blob/main/streamableHttp.ts)) that records every event with a unique ID. When reconnecting, the client sends the last received event ID via the `Last-Event-ID` header. The server then replays all messages with higher IDs from the in-memory buffer, ensuring no data loss during transient network interruptions.

### Why does SSE require a separate POST endpoint for client messages?

SSE establishes a unidirectional server-to-client channel using the `text/event-stream` MIME type. Because the browser's `EventSource` API cannot send data over that connection, the MCP implementation requires a separate HTTP POST endpoint (`/message`) for client-to-server communication, with the session ID passed as a query parameter to correlate requests with the correct SSE stream.

### Is there a performance difference between the two transports for high-frequency updates?

Streamable HTTP may introduce slightly higher memory usage due to the `InMemoryEventStore` buffering events for potential replays (lines 11-36 in [`streamableHttp.ts`](https://github.com/modelcontextprotocol/servers/blob/main/streamableHttp.ts)). However, for high-frequency scenarios where message loss is unacceptable, this overhead provides reliability guarantees that SSE cannot offer. For simple, stateless updates where dropped messages are tolerable, SSE provides lower overhead by eliminating the event store entirely.