Inicio / Artículos / Encontrar el verdadero cuello de botella en un endpoint Node.js lento

Encontrar el verdadero cuello de botella en un endpoint Node.js lento

Aprenda un método sistemático para rastrear la latencia del backend a lo largo de la ruta de solicitud, desde el código de Node.js hasta las consultas a la base de datos, utilizando medición de tiempos y EXPLAIN ANALYZE.

1671 palabras

Un endpoint de backend parece lento y la reacción inmediata es casi siempre la misma:

"Node.js es lento."

Esa solía ser también tu explicación por defecto.

Luego, después de dedicar tiempo a investigar los verdaderos problemas de rendimiento, surgió una lección diferente:

El lugar donde aparece la lentitud no es necesariamente el que la causa.

Tu servicio de Node.js puede estar funcionando tal como se planeó, mientras que la demora real proviene de la base de datos, una API de terceros, la red o una consulta mal optimizada oculta en algún punto del camino de la solicitud.

A continuación se presenta el método que vale la pena aplicar cada vez que un endpoint de backend parezca excesivamente lento.

1. Comienza con el problema real

Imagina una ruta como esta:

GET /api/users?email=user@example.com

La respuesta es correcta.

Pero consistentemente tarda alrededor de 2 a 3 segundos.

La reacción instintiva suele incluir ideas como:

  • Ajustar el código de Node.js
  • Introducir una capa de caché
  • Escalar el servidor
  • Crear instancias adicionales
  • Rescribir fragmentos de JavaScript

Nada de eso está basado en evidencias aún; son meras especulaciones.

Lo que realmente debes preguntarte primero es:

¿Dónde se está perdiendo el tiempo en realidad?

2. Mide antes de cambiar nada

En lugar de pasar directamente a modificar el código, comienza midiendo cada paso individualmente.

Por ejemplo:

console.time("getUsers");
const users = await getUsers();console.timeEnd("getUsers");

Si el resultado mostrado es:

getUsers: 2720ms

Ese único número ya te proporciona información útil.

Es probable que el cuello de botella no sea la capa HTTP en sí.

Esto está ocurriendo en algún lugar dentro de getUsers().

Es hora de profundizar un nivel más.

3. Medir la consulta a la base de datos

Supongamos que la función subyacente se ve así:

console.time("db-query");
const users = await prisma.user.findMany({
  where: {
    email: email
  }
});console.timeEnd("db-query");

Y el tiempo de ejecución resulta ser:

db-query: 2680ms

Eso reduce considerablemente el rango de búsqueda.

No es Node.js el que tarda 2.6 segundos en procesar la solicitud.

Es la llamada a la base de datos.

Este es precisamente el motivo por el cual pasar directamente a ajustes a nivel de aplicación puede hacer que se desperdicie tiempo del cual no se podrá recuperar.

4. Ahora pregúntele a PostgreSQL qué está haciendo

Este es exactamente el momento de utilizar EXPLAIN ANALYZE.

Por ejemplo:

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'user@example.com';

La salida podría revelar algo similar a esto:

Seq Scan on users
(actual time=0.025..2720.532 rows=1)

El detalle crucial a observar es:

Seq Scan

PostgreSQL recorre toda la tabla fila por fila en lugar de ir directamente al registro correspondiente a través de un índice.

Cuando la tabla crece lo suficiente, ese escaneo secuencial se vuelve realmente costoso.

5. La solución no es “optimizar Node.js”

Si las búsquedas por email ocurren con frecuencia, agregar un índice adecuado puede transformar el costo de la consulta.

Por ejemplo:

CREATE INDEX idx_users_email
ON users(email);

Ejecuta nuevamente la misma consulta después:

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'user@example.com';

El plan de ejecución debería mostrar ahora algo similar a:

Index Scan using idx_users_email

en lugar de:

Seq Scan

La cifra exacta en milisegundos no es realmente lo importante aquí.

Lo que importa es que la estrategia de ejecución se modificó basándose en evidencias medibles, no en conjeturas.

6. La lección más importante

Nada de esto se refiere fundamentalmente a PostgreSQL.

Es una lección sobre cómo depurar de manera sistemática.

Cuando una solicitud tarda en procesarse, evite culpar de inmediato al framework.

Imagínese la ruta completa que sigue una solicitud:

Client
  ↓
HTTP
  ↓
Node.js
  ↓
Business Logic
  ↓
Redis / Database / External API
  ↓
Response

Cualquier capa de esa cadena podría ser el verdadero cuello de botella.

Su tarea es identificar exactamente cuál.

7. Un proceso de depuración sencillo

Cada vez que un endpoint resulte lento, esta es más o menos la secuencia a seguir.

Paso 1: Medir toda la solicitud

Request: 2.8

Paso 2: Dividir la solicitud en componentes

Authentication: 20ms
Business logic: 50ms
Database: 2.6s
Response serialization: 15ms

Una vez que tenga este desglose, localizar el problema se vuelve mucho más sencillo.

Paso 3: Investigar el componente más lento

Si la sección de la base de datos consume 2.6 segundos:

No pierda una hora ajustando el JavaScript.

Vaya directamente a la base de datos.

Paso 4: Inspeccionar la consulta

Eche un vistazo detallado a:

EXPLAIN ANALYZE

También verifique si hay:

  • Búsquedas secuenciales
  • Si realmente se están utilizando índices
  • Uniones
  • Operaciones de ordenamiento
  • Condiciones de filtrado
  • Cuántas filas se escanean
  • Cuántas filas se devuelven realmente

Paso 5: Corregir algo

Algunas soluciones posibles:

  • Agregar un índice adecuado
  • Rescribir una consulta con estructura ineficiente
  • Eliminar una unión que no es necesaria
  • Resolver un patrón de consulta N+1
  • Reducir los datos innecesarios que se obtienen

Paso 6: Medir nuevamente

Nunca asuma ciegamente que su corrección funcionó.

Confírmelo con una nueva medición.

8. No olvide las APIs externas

El retraso no siempre se debe a la base de datos.

Considere este escenario:

const user = await getUser();
const payment = await getPaymentDetails();const orders = await getOrders();return {
  user,
  payment,
  orders
};

Si cada llamada individual tarda:

getUser()          → 100ms
getPaymentDetails() → 900ms
getOrders()         → 700ms

entonces el endpoint terminará siendo mucho más lento de lo necesario.

Y aquí, la solución no es “hacer que Node.js funcione más rápido”.

Puede tratarse simplemente de cambiar la forma en que se ejecutan las llamadas independientes.

Por ejemplo:

const [user, payment, orders] = await Promise.all([
  getUser(),
  getPaymentDetails(),
  getOrders()
]);

De esta manera, las operaciones que no dependen unas de otras se ejecutan de forma concurrente en lugar de una tras otra.

No obstante, hay una advertencia importante aquí:

No utilicePromise.all()sin pensar bien las consecuencias.

Cuando las operaciones dependen unas de otras, se requieren límites de concurrencia o existe el riesgo de sobrecargar un servicio posterior, ejecutar todo en paralelo puede empeorar las cosas en lugar de mejorarlas.

La solución adecuada para mejorar el rendimiento siempre depende de la carga de trabajo específica con la que se está trabajando.

9. Tenga cuidado con las consultas N+1

Existe otra trampa de rendimiento que a primera vista parece inofensiva.

Tomemos este ejemplo:

const users = await getUsers();
for (const user of users) {
  user.orders = await getOrders(user.id);
}

Si se está trabajando con 100 usuarios, este patrón genera silenciosamente:

1 query → get users
100 queries → get orders

Eso suma hasta 101 consultas separadas a la base de datos para atender una sola llamada API.

Este es el conocido problema de las consultas N+1.

Dependiendo de su situación, estrategias mejores podrían incluir:

  • Unir las tablas directamente
  • Depender de las funciones de carga de relaciones de un ORM
  • Obtener registros por lotes en lugar de uno a la vez
  • Usar una cláusula WHERE IN
  • Rethinking la estructura de la respuesta
  • Introducir el caché donde tenga sentido
  • Como siempre, el enfoque adecuado depende por completo de la carga de trabajo.

    10. No aumente el tamaño del servidor demasiado pronto

    Una reacción instintiva frecuente ante una API lenta es:

    "Añadamos más CPU y RAM a ella."

    Eso puede ayudar en algunos casos.

    Pero a menudo solo está gastando más dinero sin abordar el problema real.

    Si una consulta a la base de datos está mal escrita, mejorar el servidor Node.js no hará que esa consulta se ejecute más rápido.

    Antes de escalar el hardware, pregúntese:

    ¿Está la aplicación realmente limitada por la CPU o la memoria?

    Si no es así, añadir capacidad al servidor probablemente no solucionará el verdadero cuello de botella.

    11. La mentalidad de depuración que intento seguir

    Un modelo mental útil para abordar la lentitud del backend es el siguiente:

    Is the API slow?
           ↓
    Measure it
           ↓
    Which layer is slow?
           ↓
    Measure that layer
           ↓
    Find the actual bottleneck
           ↓
    Make one change
           ↓
    Measure again
    

    En lugar de:

    API slow
       ↓
    Optimize Node.js
       ↓
    Add Redis
       ↓
    Increase server
       ↓
    Hope it gets faster
    

    El segundo enfoque es una suposición.

    El primer enfoque es la ingeniería real.

    12. Mi lista de verificación para el rendimiento del backend

    Antes de realizar cualquier optimización, vale la pena confirmar:

    • ¿Cuál es el verdadero tiempo de respuesta de extremo a extremo?
    • ¿Está la carga de trabajo limitada por la CPU?
    • ¿Es lenta la consulta a la base de datos en sí?
    • ¿Hay algún patrón N+1 oculto en alguna parte?
    • ¿Están los índices adecuados en su lugar?
    • ¿Qué revela EXPLAIN ANALYZE?
    • ¿Está una API de terceros añadiendo latencia?
    • ¿Se están ejecutando tareas independientes de forma secuencial cuando no es necesario?
    • ¿El código está recuperando más datos de los que realmente necesita?
    • ¿Se está utilizando Redis donde corresponde?
    • ¿Está configurado correctamente el pool de conexiones?
    • ¿El cambio que realizó realmente afectó a los valores medidos?

    Pensamiento final

    Una de las lecciones más valiosas en el trabajo de backend es la siguiente:

    Rara vez se resuelven los problemas de rendimiento adivinando.

    Un API de Node.js lento no significa automáticamente que el propio Node.js sea el culpable.

    El verdadero responsable podría ser:

    PostgreSQL
    Redis
    External APIs
    Network
    N+1 queries
    Poor indexes
    Serialization
    Connection pools
    Application logic
    

    Saber cómo ajustar cada una de estas tecnologías por separado no es la habilidad fundamental.

    La verdadera habilidad consiste en poder identificar con precisión dónde está el cuello de botella.

    Una vez que se sabe exactamente dónde se está perdiendo el tiempo, la solución suele surgir de forma natural.

    Mide primero. Encuentra el cuello de botella. Soluciona el cuello de botella. Mide de nuevo.

    Ese es el enfoque que hoy guía la forma de abordar el rendimiento del backend.

    Lecturas relacionadas

  • Estrategia de Token de Refresco para Sistemas de Autenticación en Node.js — Aprenda cómo diseñar, rotar, revocar y almacenar de forma segura los tokens de refresco en Node.js para que el robo de tokens y el cierre de sesión funcionen tal como se espera.
  • Registro Estructurado en Node.js: Conviertiendo el Caos de la Deboguación en Producción en Claridad — Entienda por qué console.log falla en las aplicaciones Node.js en producción y cómo el registro estructurado, los niveles de registro y los IDs de correlación convierten los errores difíciles en soluciones rápidas.
  • Domar la concurrente de Node.js: Evitando colapsos de API con p-map y Bottleneck — Aprenda cómo combinar p-map y Bottleneck en Node.js para prevenir errores de límite de velocidad y sobrecarga del sistema al controlar la concurrente y el tiempo de las solicitudes.