Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Solución de problemas de RPC14 min de lectura

Reutilización de conexiones RPC y keep-alive HTTP/2: por qué las nuevas conexiones cuestan latencia

Aprende cómo los handshakes TCP/TLS, los pools keep-alive de HTTP/1.1 y la multiplexación HTTP/2 afectan la latencia, la fiabilidad y los patrones de error de JSON-RPC.

TL;DR

Cada nueva llamada JSON-RPC que abre una conexión TCP y TLS nueva paga múltiples round trips antes de enviar el primer byte de la solicitud. Reutilizar una conexión persistente HTTP/1.1 con keep-alive o multiplexar muchas llamadas sobre una única conexión HTTP/2 elimina la mayor parte de ese coste de handshake y estabiliza la latencia de cola. HTTP/1.1 requiere un pool de sockets para manejar la concurrencia, mientras que HTTP/2 usa una sola conexión con límites de streams. Los timeouts de inactividad de balanceadores de carga o NAT pueden terminar conexiones en pleno uso, causando errores espurios ECONNRESET o socket hang up. Este artículo explica los mecanismos, proporciona un ejemplo ejecutable de medición y ajuste en Node.js, y muestra cómo verificar mejoras contra tu propio endpoint.

El modelo de coste por solicitud: DNS, TCP, TLS y luego JSON-RPC

Una llamada JSON-RPC sobre HTTPS no es un único evento de red. Antes de enviar la línea de solicitud HTTP, el cliente debe resolver el nombre de host (DNS), completar un handshake TCP de tres vías y negociar TLS. Cada uno de estos pasos añade al menos un round trip al servidor, y TLS 1.3 normalmente añade un round trip después de TCP, mientras que TLS 1.2 añade dos. Solo después de establecer el canal seguro el cliente envía el POST HTTP que contiene el payload JSON-RPC.

Cuando se reutiliza una conexión, la búsqueda DNS, el handshake TCP y la negociación TLS se omiten por completo. El cliente envía la solicitud inmediatamente sobre el socket existente. Por eso la reutilización de conexiones es una de las optimizaciones de latencia de mayor impacto para clientes RPC: elimina el coste fijo por llamada que es independiente del método que se invoca.

La especificación HTTP/1.1 (RFC 9112, que obsoleta RFC 7230) define las conexiones persistentes como predeterminadas, y RFC 9110 describe la semántica de gestión de conexiones. HTTP/2 (RFC 9113) va más allá al multiplexar muchos streams de solicitud/respuesta sobre una única conexión TCP. Ambos mecanismos existen para amortizar el coste de establecimiento de conexión.

  • Resolución DNS: a menudo en caché, pero un cliente en frío la paga una vez por cada nombre de host nuevo.
  • Handshake TCP: un round trip (SYN, SYN-ACK, ACK).
  • Handshake TLS: un round trip para TLS 1.3, dos para TLS 1.2, más verificación de certificado.
  • Solicitud/respuesta HTTP: la llamada JSON-RPC real, que es lo que quieres medir.
  • Conexión reutilizada: elimina DNS, TCP y TLS de la ruta crítica para cada llamada posterior.

Keep-alive HTTP/1.1: una solicitud por conexión, así que necesitas un pool

El keep-alive de HTTP/1.1 permite que una única conexión TCP transporte múltiples pares solicitud/respuesta secuenciales. Sin embargo, HTTP/1.1 no multiplexa: solo una solicitud puede estar en vuelo en una conexión a la vez. Si se envía una segunda solicitud antes de que llegue la primera respuesta, debe esperar, causando bloqueo de cabeza de línea. Para lograr concurrencia, los clientes mantienen un pool de sockets, cada uno manejando una solicitud a la vez.

El tamaño del pool (a menudo llamado maxSockets) determina cuántas llamadas RPC concurrentes puedes hacer sin hacer cola. Si tu aplicación envía 50 llamadas concurrentes pero el pool tiene 10 sockets, 40 llamadas esperan a un socket libre. Este tiempo de espera aparece como latencia e infla p95 y p99, aunque el servidor pueda ser rápido.

El keep-alive también depende de que ambos extremos acuerden mantener la conexión abierta. El servidor o un intermediario puede enviar una cabecera Connection: close o simplemente cerrar conexiones inactivas tras un timeout. Los clientes deben manejar esto con elegancia abriendo una nueva conexión cuando sea necesario, pero las reconexiones frecuentes reintroducen costes de handshake.

  • Una solicitud en vuelo por conexión; la concurrencia requiere múltiples sockets.
  • El tamaño del pool debe coincidir con el volumen de solicitudes concurrentes esperado para evitar colas.
  • Las conexiones inactivas pueden ser cerradas por el servidor o un balanceador de carga; los clientes deben detectarlas y reemplazarlas.
  • El keep-alive HTTP/1.1 está ampliamente soportado pero es menos eficiente que HTTP/2 para alta concurrencia.

Multiplexación HTTP/2: muchos streams sobre una conexión, con límites

HTTP/2 multiplexa múltiples streams de solicitud/respuesta sobre una única conexión TCP. Esto elimina la necesidad de un pool de sockets para la concurrencia: un cliente puede enviar muchas llamadas JSON-RPC concurrentemente en una conexión, y las respuestas llegan entrelazadas. Esto reduce la sobrecarga de gestión de conexiones y mejora la latencia bajo carga porque no hay cola en el pool.

Sin embargo, HTTP/2 no es ilimitado. El servidor anuncia SETTINGS_MAX_CONCURRENT_STREAMS, que limita cuántos streams pueden estar activos a la vez. Si superas ese límite, el cliente encola streams adicionales. Además, una única conexión TCP es un único dominio de fallo: si se rompe, todos los streams en vuelo fallan. Algunos clientes mitigan esto manteniendo un pequeño número de conexiones HTTP/2, pero eso reintroduce cierta complejidad de pooling.

HTTP/2 también tiene control de flujo a nivel de conexión y de stream. Un consumidor lento o una respuesta grande pueden bloquear otros streams si se agotan las ventanas de control de flujo. Para llamadas JSON-RPC típicas, que son pequeñas, esto rara vez es un problema, pero vale la pena monitorearlo si obtienes payloads grandes.

  • Una conexión transporta muchos streams concurrentes; no se necesita un pool por socket para la concurrencia.
  • SETTINGS_MAX_CONCURRENT_STREAMS limita los streams activos; los streams en exceso se encolan.
  • Una única conexión es un único punto de fallo; considera un pequeño número de conexiones para redundancia.
  • El control de flujo puede introducir bloqueo de cabeza de línea si se agotan las ventanas.

Timeouts de inactividad y reinicios espurios: por qué ocurre ECONNRESET en pleno uso

Los balanceadores de carga, las pasarelas NAT y los proxies perimetrales de proveedores a menudo cierran conexiones TCP inactivas tras un timeout fijo (comúnmente 30–120 segundos, pero esto varía según el proveedor y está documentado / varía según el proveedor). Si tu cliente cree que la conexión sigue abierta y envía una solicitud justo cuando el intermediario la cierra, la solicitud falla con ECONNRESET o socket hang up. Esto no es una caída del proveedor; es una carrera entre tu envío y el timeout de inactividad del intermediario.

El keep-alive TCP puede ayudar enviando sondas periódicas para mantener viva la conexión, pero los intervalos de keep-alive TCP a menudo son demasiado largos (valores predeterminados de 2 horas en muchos sistemas) y deben ajustarse. Los heartbeats a nivel de aplicación (por ejemplo, una llamada JSON-RPC ligera como eth_blockNumber o getHealth) son más fiables porque generan tráfico real que reinicia los temporizadores de inactividad en todos los intermediarios.

Para HTTP/2, el servidor puede enviar frames GOAWAY para cerrar una conexión de forma ordenada. Los clientes deben manejar GOAWAY terminando los streams en vuelo y abriendo una nueva conexión para solicitudes posteriores. Ignorar GOAWAY provoca solicitudes fallidas.

  • Los timeouts de inactividad son comunes en balanceadores de carga y NATs; cierran conexiones sin notificar al cliente.
  • El keep-alive TCP ayuda pero los intervalos predeterminados a menudo son demasiado largos; ajústalos o usa heartbeats de aplicación.
  • GOAWAY de HTTP/2 indica cierre de conexión; los clientes deben reconectar.
  • ECONNRESET es a menudo un síntoma de timeout de inactividad, no un fallo del proveedor.

Keep-alive y suscripciones WebSocket: la inactividad es el enemigo

Las suscripciones WebSocket (por ejemplo, accountSubscribe de Solana o eth_subscribe de Ethereum) usan una conexión de larga duración que está inactiva por diseño: el servidor envía datos solo cuando ocurren eventos. Esto las hace vulnerables a los timeouts de inactividad. A diferencia del HTTP de solicitud/respuesta, donde simplemente puedes reintentar, una suscripción caída significa eventos perdidos hasta que el cliente reconecta y se vuelve a suscribir.

Para mantener vivas las suscripciones, los clientes deben enviar frames ping/pong periódicos (a nivel de protocolo WebSocket) o heartbeats a nivel de aplicación. Muchos proveedores RPC documentan un tiempo máximo de inactividad para conexiones WebSocket; superarlo resulta en desconexión. Implementa siempre lógica de reconexión con retroceso exponencial y vuelve a suscribirte al reconectar.

HTTP/2 no se usa para suscripciones WebSocket en la mayoría de configuraciones JSON-RPC; WebSocket se ejecuta sobre su propia conexión TCP tras un upgrade HTTP. Por lo tanto, las estrategias de reutilización de conexiones para HTTP/2 no aplican a las suscripciones. Trátalas como preocupaciones separadas.

El comportamiento de multiplexación y concurrencia de streams aquí descrito lo define la especificación HTTP/2, RFC 9113; consultarla para la semántica exacta de SETTINGS_MAX_CONCURRENT_STREAMS y control de flujo que tu cliente debe respetar.

  • Las suscripciones WebSocket son de larga duración e inactivas; los timeouts de inactividad causan desconexiones.
  • Usa ping/pong o heartbeats de aplicación para mantener viva la conexión.
  • Implementa lógica de reconexión y resuscripción; los eventos perdidos no se reproducen.
  • La multiplexación HTTP/2 no aplica a las suscripciones WebSocket.

Antipatrones en serverless y cliente nuevo por solicitud

Las funciones serverless (AWS Lambda, Cloudflare Workers, etc.) a menudo crean un nuevo cliente HTTP por invocación, lo que significa un nuevo handshake TCP y TLS por cada llamada RPC. Esto desperdicia decenas a cientos de milisegundos por llamada e infla la latencia p95. Algunos runtimes permiten reutilizar clientes entre invocaciones mediante variables globales, pero los arranques en frío siguen pagando el coste del handshake.

De manera similar, crear una nueva instancia de cliente por solicitud en un servidor de larga duración es un antipatrón. El cliente debe instanciarse una vez y reutilizarse. Si tu framework crea un nuevo cliente por solicitud de forma predeterminada, anula ese comportamiento. La reutilización de conexiones es responsabilidad del lado del cliente; el servidor no puede ayudar si sigues llamando a la puerta con un nuevo handshake.

Para serverless, considera usar un proveedor que soporte HTTP/2 y keep-alive, y configura tu cliente para reutilizar conexiones dentro del entorno de ejecución. Si los arranques en frío son inevitables, mide el coste del handshake e inclúyelo en tu presupuesto de latencia.

  • Cliente nuevo por solicitud = nuevo handshake por solicitud = latencia desperdiciada.
  • Reutiliza instancias de cliente entre solicitudes en servidores de larga duración.
  • Los arranques en frío serverless pagan el coste del handshake; reutiliza clientes entre invocaciones cuando sea posible.
  • Mide el coste del handshake para entender su impacto en tu p95.

Límites de tasa: el número de conexiones no es el límite, el peso del método sí

Los proveedores RPC normalmente limitan la tasa según el recuento de solicitudes o el peso del método, no según el número de conexiones TCP. Agrupar conexiones reduce la latencia pero no aumenta tu límite de tasa. Si superas el límite, recibirás respuestas 429 independientemente de cuántas conexiones uses. A la inversa, usar menos conexiones no reduce tu consumo de límite de tasa.

Algunos proveedores pueden tener límites separados en conexiones o streams concurrentes, pero estos suelen ser generosos en comparación con los límites de solicitudes. La conclusión clave: optimiza la reutilización de conexiones para la latencia, pero no esperes que aumente tu techo de rendimiento. Para el rendimiento, considera el batching (ver Solicitudes batch JSON-RPC y mejores prácticas) o mejorar tu plan (ver Precios de RPC).

Si estás alcanzando los límites de tasa, la solución es reducir el recuento de solicitudes (batching, caché) o aumentar tu plan, no abrir más conexiones.

  • Los límites de tasa se basan en solicitudes o peso del método, no en el número de conexiones.
  • El pooling de conexiones mejora la latencia, no el margen del límite de tasa.
  • El batching y la caché reducen el recuento de solicitudes y ayudan a mantenerse por debajo de los límites.
  • Consulta la documentación de tu proveedor para la semántica específica de límites de tasa.

Ejemplo ejecutable en Node.js: Agent de undici con keep-alive y bucle de medición

El siguiente script de Node.js usa undici (el cliente HTTP/1.1 usado por fetch de Node.js) para crear un Agent con keep-alive habilitado, un pool maxSockets y un keepAliveTimeout. Luego mide la latencia de la primera solicitud (que paga el handshake) frente a las solicitudes posteriores (que reutilizan la conexión). Reemplaza RPC_URL con tu endpoint.

El script también demuestra cómo establecer un keepAliveTimeout corto para evitar timeouts de inactividad, y cómo medir la diferencia. Ejecútalo con Node.js 18+ (que incluye undici). La salida mostrará que la primera solicitud tarda significativamente más que las solicitudes en estado estable, ilustrando el coste del handshake.

const { Agent, request } = require('undici');

const RPC_URL = 'https://your-rpc-endpoint.example.com';
const agent = new Agent({
  keepAliveTimeout: 10_000, // 10 seconds; tune to be less than intermediary idle timeout
  keepAliveMaxTimeout: 60_000,
  maxSockets: 20, // pool size for HTTP/1.1 concurrency
  pipelining: 1, // HTTP/1.1 pipelining is generally not recommended
});

async function rpcCall(method, params = []) {
  const start = process.hrtime.bigint();
  const { statusCode, body } = await request(RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
    dispatcher: agent,
  });
  const text = await body.text();
  const end = process.hrtime.bigint();
  const ms = Number(end - start) / 1e6;
  return { statusCode, ms, text };
}

(async () => {
  console.log('First request (pays handshake):');
  const first = await rpcCall('eth_blockNumber');
  console.log(`  ${first.ms.toFixed(2)} ms, status ${first.statusCode}`);

  console.log('Steady-state requests (reuse connection):');
  for (let i = 0; i < 5; i++) {
    const res = await rpcCall('eth_blockNumber');
    console.log(`  ${res.ms.toFixed(2)} ms, status ${res.statusCode}`);
  }

  await agent.close();
})();

Método de medición: tabla de latencia de primera solicitud vs estado estable

Para cuantificar el beneficio de la reutilización de conexiones en tu endpoint, ejecuta un bucle de medición que registre la latencia de la primera solicitud (conexión en frío) y las siguientes N solicitudes (conexión caliente). Usa el mismo método y parámetros para todas las llamadas. Ejecuta la prueba varias veces para tener en cuenta la variabilidad de la red. La tabla a continuación es una plantilla; complétala con tus propias mediciones.

Registra la mediana y el p95 de las solicitudes en caliente, y compáralos con la solicitud en frío. La diferencia es el coste del handshake. Si la diferencia es grande en relación con tu presupuesto de latencia, la reutilización de conexiones es una optimización de alta prioridad. Mide también el efecto de diferentes valores de maxSockets bajo concurrencia para encontrar el punto donde desaparece la cola.

  • Ejecuta el script anterior contra tu endpoint.
  • Registra la latencia de la primera solicitud (fría) y las siguientes 20 solicitudes (calientes).
  • Calcula la mediana y el p95 de las solicitudes en caliente.
  • Repite con diferentes valores de maxSockets (por ejemplo, 5, 10, 20, 50) bajo una carga concurrente de 50 solicitudes.
  • Documenta los resultados en una tabla como la siguiente.
| Run | Cold request (ms) | Warm median (ms) | Warm p95 (ms) | maxSockets | Concurrent requests |
|-----|-------------------|------------------|---------------|------------|---------------------|
| 1   |                   |                  |               | 10         | 50                  |
| 2   |                   |                  |               | 20         | 50                  |
| 3   |                   |                  |               | 50         | 50                  |

Guía de ajuste: pooling HTTP/1.1 vs multiplexación HTTP/2

Los parámetros de ajuste difieren entre HTTP/1.1 y HTTP/2. Para HTTP/1.1, la clave es el tamaño del pool (maxSockets) y keepAliveTimeout. Para HTTP/2, la clave es el número de conexiones (normalmente 1–2) y el manejo de SETTINGS_MAX_CONCURRENT_STREAMS. La tabla a continuación resume qué ajustar.

Para HTTP/1.1, establece maxSockets al menos a tu recuento esperado de solicitudes concurrentes. Establece keepAliveTimeout a un valor menor que el timeout de inactividad del intermediario (por ejemplo, 10–30 segundos). Para HTTP/2, usa una única conexión si el servidor soporta suficientes streams concurrentes; de lo contrario, usa un pequeño número de conexiones. Monitorea frames GOAWAY y reconecta según sea necesario.

  • HTTP/1.1: ajusta maxSockets (tamaño del pool) y keepAliveTimeout.
  • HTTP/2: ajusta el número de conexiones y respeta SETTINGS_MAX_CONCURRENT_STREAMS.
  • Ambos: implementa reintentos con retroceso para ECONNRESET y GOAWAY.
  • Ambos: usa heartbeats a nivel de aplicación si los timeouts de inactividad son agresivos.
| Protocol | Key parameter          | Typical starting value | Notes                                      |
|----------|------------------------|------------------------|--------------------------------------------|
| HTTP/1.1 | maxSockets             | 20–50                  | Match expected concurrency                 |
| HTTP/1.1 | keepAliveTimeout       | 10–30 s                | Less than intermediary idle timeout        |
| HTTP/2   | connections            | 1–2                    | One connection multiplexes many streams    |
| HTTP/2   | maxConcurrentStreams   | Server-advertised      | Client should not exceed                   |

Solución de problemas: fallos comunes y cómo diagnosticarlos

Si ves ECONNRESET o socket hang up, primero verifica si se correlaciona con períodos de inactividad. Si ocurre tras un período de inactividad, probablemente sea un timeout de inactividad. Aumenta la frecuencia de heartbeat o reduce keepAliveTimeout. Si ocurre bajo carga, verifica si estás superando maxSockets y haciendo cola, o si el servidor está cerrando conexiones debido a límites de tasa.

Si deshabilitas accidentalmente el keep-alive (por ejemplo, estableciendo Connection: close en un valor predeterminado del framework), pagarás costes de handshake en cada solicitud. Revisa la configuración de tu cliente. Si usas un balanceador de carga que reinicia conexiones inactivas, asegúrate de que tu keepAliveTimeout sea menor que el timeout de inactividad del balanceador. Si asumes que una conexión da más rendimiento que un pool acotado, mide: bajo HTTP/1.1, una conexión serializa las solicitudes, por lo que un pool es necesario para la concurrencia.

Malinterpretar ECONNRESET como una caída del proveedor es común. Antes de escalar, verifica con un curl simple o un script que reutilice conexiones. Si el error desaparece con la reutilización de conexiones, fue un timeout de inactividad, no una caída. Para más información sobre timeouts, consulta Errores de timeout RPC: causas y soluciones.

  • ECONNRESET tras un período de inactividad → timeout de inactividad; añade heartbeats o reduce keepAliveTimeout.
  • ECONNRESET bajo carga → revisa maxSockets y límites de tasa.
  • Keep-alive deshabilitado por el framework predeterminado → revisa la configuración del cliente.
  • El balanceador de carga reinicia conexiones inactivas → establece keepAliveTimeout menor que el timeout de inactividad del balanceador.
  • Asumir que una conexión es suficiente para la concurrencia → mide y usa un pool para HTTP/1.1.

Limitaciones, compensaciones y cuándo revisar

La reutilización de conexiones no es una bala de plata. Reduce la latencia del handshake pero no reduce el tiempo de procesamiento del servidor ni la latencia de red entre tú y el proveedor. Si tus llamadas RPC son lentas debido a la carga del lado del servidor, la reutilización de conexiones no lo solucionará. Además, una única conexión HTTP/2 es un único punto de fallo; si se rompe, todas las solicitudes en vuelo fallan. Algunos clientes mitigan esto con múltiples conexiones, pero eso añade complejidad.

Los timeouts de inactividad son específicos del proveedor y pueden cambiar. Lo que funciona hoy puede no funcionar mañana si el proveedor ajusta su infraestructura. Monitorea tus tasas de error y latencia, y prepárate para ajustar keepAliveTimeout e intervalos de heartbeat. Para suscripciones WebSocket, la lógica de reconexión es obligatoria independientemente de la configuración de keep-alive.

Revisa tu configuración cuando cambies de proveedor, cuando veas nuevos patrones de error o cuando tu perfil de concurrencia cambie. Usa el método de medición anterior para validar que tu configuración sigue entregando el beneficio esperado. Para una visión más amplia de las causas de latencia, consulta Latencia RPC: causas, medición y soluciones.

  • La reutilización de conexiones no reduce el procesamiento del servidor ni la latencia de red.
  • Una única conexión HTTP/2 es un único punto de fallo.
  • Los timeouts de inactividad son específicos del proveedor y pueden cambiar.
  • Revisa la configuración al cambiar de proveedor o de perfil de concurrencia.
  • Implementa siempre reconexión para suscripciones WebSocket.

Próximos pasos: aplicar la reutilización de conexiones a tu stack

Empieza midiendo el coste del handshake en tu endpoint usando el script anterior. Si la solicitud en frío es significativamente más lenta que las solicitudes en caliente, habilita keep-alive y ajusta el tamaño de tu pool. Para HTTP/1.1, establece maxSockets para que coincida con tu concurrencia. Para HTTP/2, usa una única conexión y respeta los límites de streams. Implementa heartbeats para suscripciones WebSocket.

Si usas OnFinality, revisa la Guía de endpoints RPC para detalles específicos de endpoints. Para consideraciones de latencia específicas de Solana, consulta Latencia RPC de Solana: medición y optimización. Para una visión general de las ofertas RPC de OnFinality, visita el hub de aprendizaje de OnFinality y el servicio API.

Finalmente, recuerda que la reutilización de conexiones es una parte de una estrategia de rendimiento más amplia. Combínala con batching, caché y una gestión adecuada de límites de tasa. Mide, ajusta y monitorea continuamente.

  • Mide el coste del handshake en tu endpoint.
  • Habilita keep-alive y ajusta el tamaño del pool o las conexiones HTTP/2.
  • Implementa heartbeats para suscripciones WebSocket.
  • Combina con batching y caché para mejores resultados.
  • Monitorea y revisa a medida que cambia tu uso.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar