Por qué los WebSockets se bloquean en las rutas API de Next.js y cómo un servidor personalizado lo soluciona
Aprenda por qué un servidor WS dentro de una ruta pages/api nunca completa el intercambio de mensajes, y cómo manejar el evento de actualización por su cuenta con un servidor Next.js personalizado.
Fuentes: The Next.js WebSocket Trap: Why Your pages/api Handler Never Upgrades
Un intento común inicial con WebSockets en Next.js es crear un servidor ws dentro de pages/api/socket.ts y conectarse a él desde el navegador. El resultado es confuso: no hay error, ni mensaje, solo un socket en estado CONNECTING hasta que el navegador agota el tiempo de espera. Esta guía explica por qué ocurre esto, muestra cómo tomar el control del servidor HTTP para que el intercambio de datos se complete realmente, y aborda los límites de escalado y alojamiento que debes aceptar a cambio.
Por qué una ruta API nunca puede mejorar la conexión
Una ruta API de Next.js es un manejador de solicitudes HTTP ordinario. Se ejecuta una vez que la solicitud ya ha sido enrutada como HTTP estándar. Sin embargo, un handshake de WebSocket es una solicitud HTTP que lleva un encabezado Upgrade, y debe ser capturado en el evento upgrade del servidor Node, antes de que cualquier router lo procese. Para cuando se ejecuta la función de la ruta, esa oportunidad ya ha pasado. La ruta responde con JSON mientras el cliente espera una respuesta 101 Switching Protocols, y ninguna de las partes logra avanzar.
La versión defectuosa comienza con las importaciones habituales, incluidos los tipos API de Next:
// pages/api/socket.ts — DOES NOT WORK
import { WebSocketServer } from "ws";
import type { NextApiRequest, NextApiResponse } from "next";
Luego, la ruta crea un WebSocketServer y adjunta un manejador de conexiones que repite los mensajes, pero su manejador exportado simplemente devuelve un estado en JSON.
const wss = new WebSocketServer({ noServer: true });wss.on("connection", (ws) => {
ws.on("message", (msg) => ws.send(`echo: ${msg}`));
});export default function handler(_req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ status: "ws server" });
}
La instancia wss existe, y noServer: true es incluso la configuración correcta. Lo que falta es alguna llamada a handleUpgrade. Nada transfiere el socket a ws, por lo que new WebSocket(...) del navegador nunca finaliza el proceso de handshake.
Asumir el control del servidor HTTP
La solución es dejar de esperar que una ruta API haga esto y controlar directamente el servidor HTTP. Next.js admite un servidor personalizado: usted mismo crea el servidor HTTP de Node, envía las solicitudes normales al manejador de solicitudes de Next y escucha los eventos upgrade por su cuenta.
// server.ts
import { createServer } from "node:http";
import next from "next";
import { WebSocketServer, type WebSocket } from "ws";
import { parse } from "node:url";
El script del servidor a continuación realiza cuatro cosas:
- Inicia Next en modo de desarrollo o producción y obtiene el manejador de solicitudes predeterminado.
WebSocketServer con noServer: true y hace un seguimiento de los sockets abiertos en un Set, eliminándolos cuando ocurren eventos close o error.app.prepare() finaliza, se crea el servidor HTTP y, en el listener upgrade, se verifica la ruta: /api/ws se pasa a wss.handleUpgrade, mientras que cualquier otra ruta es descartada. Finalmente, el servidor escucha en la puerta PORT o 3000.const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();const wss = new WebSocketServer({ noServer: true });
const clients = new Set<WebSocket>();wss.on("connection", (ws) => {
clients.add(ws);
ws.on("message", (data) => {
const text = data.toString();
for (const client of clients) {
if (client.readyState === client.OPEN) client.send(text);
}
});
ws.on("close", () => clients.delete(ws));
ws.on("error", () => clients.delete(ws));
});app.prepare().then(() => {
const server = createServer((req, res) => handle(req, res, parse(req.url!, true))); server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url ?? "/", "http://localhost");
if (pathname === "/api/ws") {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
} else {
socket.destroy();
}
}); const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => console.log(`ready on http://localhost:${port}`));
});
Los dos detalles que definen el diseño
En primer lugar, noServer: true indica a ws que no abra un listener propio, ya que se le proporcionarán manualmente las solicitudes de actualización.
En segundo lugar, la verificación de la ruta es más importante de lo que parece. Sin ella, toda solicitud de actualización que llegue al servidor se enviaría a tu handleUpgrade, incluido el socket de reemplazo de módulos en tiempo real que Next utiliza durante el desarrollo. Esta protección mantiene separado tu endpoint /api/ws. Sin embargo, ten en cuenta que la rama else destruye todas las demás actualizaciones; dependiendo de tu versión de Next.js, esto también podría interrumpir la conexión HMR de desarrollo. Si el recargo en tiempo real deja de funcionar, redirige las actualizaciones que no coincidan a Next en lugar de destruirlas (revisa la sección del servidor personalizado en la documentación actual de Next.js para conocer la forma recomendada de hacerlo).
El orden de los argumentos en handleUpgrade es fácil de equivocar. Según lo documentado para ws 8.18.3, es handleUpgrade(request, socket, head, callback). Dentro del callback, usted mismo emite connection en el servidor, por lo que el manejador wss.on("connection") se ejecuta exactamente como lo haría en un servidor independiente.
El componente del cliente
La parte del navegador está diseñada de forma sencilla. Como componente de cliente, comienza con la directiva "use client" y los hooks de React que necesita.
"use client";
import { useEffect, useState } from "react";
LiveFeed abre un socket hacia la misma ruta que protege el servidor, agrega cada mensaje recibido al estado y muestra la lista.
export function LiveFeed() {
const [messages, setMessages] = useState<string[]>([]); useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${window.location.host}/api/ws`); ws.onmessage = (event) => {
setMessages((prev) => [...prev, event.data as string]);
}; return () => ws.close();
}, []); return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}
Existen dos opciones que evitan errores reales:
- El protocolo se deriva de
window.location.protocolen lugar de codificar directamentews://. En producción, la página se sirve a través de HTTPS, y los navegadores bloquean una conexiónws://no encriptada desde una páginahttps://; por lo tanto, codificarla directamente causaría errores solo después del despliegue. - El efecto devuelve una función de limpieza que llama a
ws.close(). En desarrollo, el Modo Estricto de React ejecuta los efectos dos veces, así que sin esa función de limpieza se perdería un socket adicional en cada montaje.
Ejecutar el servidor
Inicie la aplicación con tsx server.ts, o su equivalente compilado en producción, en lugar de next dev. Renunciar a la orden de desarrollo sin configuración a cambio de tener el control del servidor HTTP representa el verdadero costo de los WebSockets nativos en este entorno.
Verificar el comportamiento
Una verificación manual rápida, realizada en Node 24 con ws 8.18.3, confirma cada aspecto:
- Cuando hay dos clientes conectados, un mensaje como
hello from aenviado por el primero llega al segundo a través del bucle de difusión. - Un socket abierto en cualquier ruta distinta a
/api/wsfalla de inmediato en lugar de quedar atascado, ya quesocket.destroy()lo cierra al instante en lugar de dejarlo en estado indefinido como ocurre con la versión basada en rutas API. - Una ruta HTTP ordinaria en el mismo servidor sigue devolviendo su contenido, lo que demuestra que el manejo de solicitudes de Next y la actualización a WebSocket comparten un mismo receptor.
Dónde deja de ser escalable este enfoque
Un Set en memoria de clientes dentro de un único proceso funciona bien hasta que se ejecutan más de una instancia. Con dos pods detrás de un balanceador de carga, un mensaje recibido en el pod A nunca llega a un cliente conectado al pod B, ya que cada proceso tiene su propio conjunto de clients. En ese caso se necesita una capa de pub/sub, generalmente Redis, para distribuir los mensajes entre las instancias, o un servicio en tiempo real gestionado. Para conocer más sobre salas, persistencia y distribución de mensajes, consulte diseño de backends para chats en tiempo real. No intente implementarlo desde el primer día: un proceso Node puede manejar muchos sockets concurrentes, y muchas aplicaciones nunca superan esa capacidad.
Restricciones de alojamiento
Un servidor personalizado también te aleja de parte del modelo de despliegue sin servidor de Next. Los WebSockets requieren un proceso de larga duración, no una invocación de función como la de Lambda. Si tu host solo ejecuta funciones sin servidor, los WebSockets no funcionarán allí independientemente de cómo escribas el manejador, así que confirma esto antes de invertir en esta función.
Puntos clave
- El intercambio de mensajes de WebSocket tiene lugar en el evento
upgradedel servidor, algo que una ruta API nunca ve. - Un servidor personalizado te permite redirigir las actualizaciones a
wsconnoServer: trueyhandleUpgrade, mientras que Next se encarga del HTTP normal. - Protege las actualizaciones según la ruta de acceso y sé consciente de lo que ocurre con aquellas que no son tuyas.
- Determina si se trata de
wsowssa partir del protocolo de la página y cierra siempre los sockets como parte de las tareas de limpieza.
Lecturas relacionadas
- Dónde dibujar la línea servidor-cliente en una página del App Router de Next.js — Un modelo mental práctico para los Componentes Servidor de React: qué se ejecuta dónde, cómo se divide una página de artículo en partes servidor y cliente, y las reglas de importación a seguir.
- ¿Acciones servidor o manejadores de ruta? Una guía de toma de decisiones para Next.js 16 — Aprenda cuándo una mutación en Next.js 16 debe ir en una Acción Servidor y cuándo necesita un Manejador de Ruta, con ejemplos corregidos para formularios, errores, optimismo y webhooks.
- Dónde deben estar la autenticación, la carga de datos y los errores en una app React: diez soluciones — Diez errores comunes al manejar la autenticación, la carga de datos, los errores y los efectos secundarios del estado en React, y cómo los interceptores, cargadores, cachés de consultas y límites de errores los solucionan.
- Fortaleciendo un servidor MCP de TypeScript para tráfico de red real — Cómo pasar un servidor MCP de stdio a Streamable HTTP con aislamiento por sesión, autenticación OAuth bearer, límites de velocidad, límites de errores y defensas contra inyecciones.
- Dónde trazar la línea servidor-cliente en una página del App Router de Next.js — Un modelo mental práctico para los Componentes Servidor de React: qué se ejecuta dónde, cómo una página de artículo de blog se divide en partes del servidor y del cliente, y las reglas de importación a seguir.
- ¿Acciones de servidor o controladores de ruta? Una guía de decisión para Next.js 16 — Aprenda cuándo una mutación de Next.js 16 debe colocarse en una acción del servidor y cuándo necesita un manejador de ruta, con ejemplos corregidos para formularios, errores, optimismo y webhooks.
- Elegir el transporte y la arquitectura para aplicaciones JavaScript en tiempo real — Cómo se integran WebSockets, Socket.IO, WebRTC, los brokers de mensajes, las regiones periféricas y la supervisión al desarrollar chats, transmisiones en vivo o juegos multijugador en JavaScript.
- MCP sin estado: Escalado de servidores sin sesiones ni intercambios de datos — Cómo un Protocolo de Contexto de Modelo sin estado elimina sesiones y sincronizaciones, y cómo las solicitudes _meta de múltiples rondas, los encabezados de enrutamiento, el caché y las tareas mantienen su utilidad.
- Tiempo de navegador, servidor o compilación: un mapa de decisiones para las arquitecturas frontend — Vea cómo SSG, SSR, streaming, Server Components, BFFs, renderizado en el edge, monolitos modulares y microfrontends responden cada uno a una misma pregunta: ¿dónde debe realizarse el trabajo?
- Mida primero: por qué la optimización temprana hace que las aplicaciones Next.js sean más difíciles de ejecutar — Vea cómo la memorización prematura, los límites amplios de los clientes y las cachés en capas añaden complejidad a las aplicaciones Next.js, y cómo un flujo de trabajo centrado en la medición mantiene su velocidad.