Skip to content
For the complete Netlify documentation index, see llms.txt. Markdown versions of any documentation page are available by appending .md to its URL.

Real-time

Because your server is a long-lived process, it can hold connections open with visitors for as long as they need. Use WebSockets for two-way communication, such as chat or multiplayer, and Server-Sent Events or a streamed response to send updates as they happen, such as a model's output.

Build with Agent Runners

Builds a working version of this demo on your own site.

How you accept WebSocket connections depends on what your entry file exports.

Upgrade the request with upgradeWebSocket. You get back a browser-style WebSocket to talk to the client, and a Response to return from your handler:

netlify/server/index.ts
import { upgradeWebSocket } from "@netlify/server";
export default async (req: Request) => {
if (req.headers.get("upgrade") === "websocket") {
const { socket, response } = upgradeWebSocket(req);
socket.onmessage = (event) => {
socket.send(`Echo: ${event.data}`);
};
return response;
}
return new Response("Connect with a WebSocket client");
};

Attach a WebSocket library such as ws or Socket.IO to your server, as you would anywhere else:

netlify/server/index.mjs
import express from "express";
import { WebSocketServer } from "ws";
const app = express();
const server = app.listen(process.env.PORT);
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
ws.on("message", (data) => ws.send(data));
});

An app you export directly, such as an Express app, can't accept WebSocket connections, and upgrade requests to it are answered with a 501. Start it with listen() instead, as in the example above.

To send a stream of updates, respond with a text/event-stream body and keep writing to it. Netlify sends each event to the visitor as soon as your server writes it.

netlify/server/index.ts
export default async (req: Request) => {
const encoder = new TextEncoder();
let timer: ReturnType<typeof setInterval>;
const stream = new ReadableStream({
start(controller) {
timer = setInterval(() => {
controller.enqueue(encoder.encode(`data: ${new Date().toISOString()}\n\n`));
}, 1000);
},
cancel() {
clearInterval(timer);
},
});
return new Response(stream, {
headers: { "content-type": "text/event-stream" },
});
};

Any other streamed response works the same way, such as a model's output streamed as it's generated.

WebSocket and Server-Sent Events connections both close when they reach a limit or when an instance stops, so make your clients reconnect. The browser's EventSource reconnects to a Server-Sent Events stream for you. A WebSocket doesn't, so reconnect when it closes:

function connect() {
const socket = new WebSocket("wss://example.com/api/chat");
socket.addEventListener("close", (event) => {
// 1012 means the server is restarting, so reconnect right away.
setTimeout(connect, event.code === 1012 ? 0 : 1000);
});
return socket;
}

The new connection reaches a running instance, which can be a new one if the previous instance stopped.

When an instance shuts down, its open connections can stay open for up to 90 seconds. Close them in a shutdown hook instead, so clients move to a new instance straight away. With upgradeWebSocket, getWebSockets returns every open connection:

netlify/server/index.ts
import { getWebSockets, onShutdown } from "@netlify/server";
onShutdown(() => {
for (const socket of getWebSockets()) {
socket.close(1012, "Server restarting");
}
});

With a library such as ws, close the connections it keeps track of instead, such as wss.clients.

The two kinds of connection count differently toward an instance's limits:

Either kind of open connection:

Each instance holds its own connections. If clients need to see the same state, such as the messages in a chat room, keep that state in Netlify Database or Netlify Blobs rather than only in an instance's memory, so it survives the instance being replaced.