MongoDB + NodeJS: Tiempo de Respuesta de una API (VPS)
13 minuto(s)En esta página:
Para esta prueba vamos a hacer uso de un VPS real, para obtener resultados reales en producción
Al menos pensemos en los que están comenzando y que ayude a todos los usuarios en general.
Si tienes un VPS potente o menor que este, puedes hacer las mismas pruebas, solo cambiará el tiempo de respuesta de tu API.
El VPS que usaré tiene las siguientes características de hardware:
- CPU : 2 vCores
- RAM : 2GB
- Disk : 60GB
Vamos a usar una base de datos con 50 registros o datos de productos.
1. Creación de la API
Vamos a realizar pruebas reales como si estuvieramos en producción de las 4 operaciones básicas CRUD (Create, Read, Update y Delete).
1.1 Código de Node JS
Creo un archivo llamado server.js con el siguiente contenido:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const app = express(); const PORT = 3071; // Middleware app.use(cors()); app.use(express.json()); // Permite procesar JSON en el cuerpo de las peticiones (req.body) // Conexión a MongoDB const MONGO_URI = 'mongodb://127.0.0.1:27017/test_crud'; mongoose.connect(MONGO_URI) .then(() => console.log('✓ Conectado exitosamente a MongoDB local')) .catch(err => console.error('✗ Error al conectar a MongoDB:', err)); // 1. Definición del Modelo de Producto const ProductoSchema = new mongoose.Schema({ nombre: { type: String, required: true }, precio: { type: Number, required: true }, categoria: { type: String, required: true }, stock: { type: Number, default: 0 }, descripcion: String }, { timestamps: true }); // AgregacreatedAt y updatedAt automáticamente const Producto = mongoose.model('Producto', ProductoSchema); // ========================================== // RUTAS CRUD (API v1) // ========================================== // [GET] Obtener todos los productos app.get('/api/v1/productos', async (req, res) => { try { const productos = await Producto.find().sort({ createdAt: -1 }); res.json(productos); } catch (error) { res.status(500).json({ mensaje: 'Error al obtener los productos', error: error.message }); } }); // [GET] Obtener un solo producto por ID app.get('/api/v1/productos/:id', async (req, res) => { try { const producto = await Producto.findById(req.params.id); if (!producto) return res.status(404).json({ mensaje: 'Producto no encontrado' }); res.json(producto); } catch (error) { res.status(500).json({ mensaje: 'Error al buscar el producto', error: error.message }); } }); // [POST] Crear un nuevo producto app.post('/api/v1/productos', async (req, res) => { try { const { nombre, precio, categoria, stock, descripcion } = req.body; if (!nombre || !precio || !categoria) { return res.status(400).json({ mensaje: 'Faltan campos obligatorios (nombre, precio, categoria)' }); } const nuevoProducto = new Producto({ nombre, precio, categoria, stock, descripcion }); await nuevoProducto.save(); res.status(201).json(nuevoProducto); } catch (error) { res.status(500).json({ mensaje: 'Error al crear el producto', error: error.message }); } }); // [PUT] Actualizar un producto por ID app.put('/api/v1/productos/:id', async (req, res) => { try { const productoActualizado = await Producto.findByIdAndUpdate( req.params.id, req.body, { new: true, runValidators: true } // 'new' devuelve el objeto ya modificado ); if (!productoActualizado) return res.status(404).json({ mensaje: 'Producto no encontrado' }); res.json(productoActualizado); } catch (error) { res.status(500).json({ mensaje: 'Error al actualizar el producto', error: error.message }); } }); // [DELETE] Eliminar un producto por ID app.delete('/api/v1/productos/:id', async (req, res) => { try { const productoEliminado = await Producto.findByIdAndDelete(req.params.id); if (!productoEliminado) return res.status(404).json({ mensaje: 'Producto no encontrado' }); res.json({ mensaje: 'Producto eliminado correctamente', producto: productoEliminado }); } catch (error) { res.status(500).json({ mensaje: 'Error al eliminar el producto', error: error.message }); } }); // Inicialización del servidor app.listen(PORT, () => { console.log(`🚀 Servidor API corriendo en puerto ${PORT}`); }); |
1.2 Instalamos las dependencias:
Usaremos Express y Mongoose:
|
1 2 3 |
npm install express mongoose |
1.3 Crear y poblar la Base de Datos
Ingresamos a la CLI de MongoDB desde la terminal:
|
1 2 3 |
mongosh |
Nota: Si tu instalación es un poco más antigua y mongosh te dice “command not found”, intenta con el comando clásico:
|
1 2 3 |
mongo |
Si tu base de datos requiere credenciales (usuario y contraseña), debes conectarte especificando la base de datos de autenticación (que suele ser admin):
Una vez que veas que el prompt de la terminal cambia (generalmente muestra un > o test>), ya estás dentro de MongoDB. Usa estos comandos para explorarla:
En MongoDB, una base de datos se crea automáticamente en el momento en que guardas el primer dato. Vamos a llamarla test_crud y le insertaremos una lista de 50 productos simulados usando un bucle de JavaScript nativo en la consola:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# Con este comando creamos y nos cambiamos a la nueva base de datos use test_crud # Insertamos los 50 documentos de golpe en la colección "productos" const productosPrueba = []; for (let i = 1; i <= 50; i++) { productosPrueba.push({ nombre: `Producto de Prueba ${i}`, precio: Math.floor(Math.random() * 100) + 10, categoria: i % 2 === 0 ? "Electrónica" : "Hogar", stock: Math.floor(Math.random() * 50) + 1, descripcion: `Descripción del producto de prueba ${i}`, createdAt: new Date(), updatedAt: new Date() }); } db.productos.insertMany(productosPrueba); |
Al presionar Enter, la terminal te devolverá un mensaje confirmando que se insertaron los 50 registros (acknowledged: true junto con los IDs generados), puedes verificar que existan ejecutando: db.productos.countDocuments().
2. Encendiendo el servidor
Para que tu API corra en un entorno de producción real dentro de tu infraestructura, el uso de PM2 es la mejor decisión. PM2 se encargará de mantener la aplicación viva en segundo plano, reiniciarla si el VPS sufre un pico de carga o si el proceso se cae por algún error, y gestionar los logs.
2.1 Instalar PM2 de forma Global (Si no lo tienes)
De paso te sirve para verificar que tengas PM2 instalado globalmente en el Node.js de tu servidor:
|
1 2 3 |
npm install -g pm2 |
2.2 Iniciar la API con PM2
Navega hasta la carpeta del proyecto y enciende el proceso dándole un nombre descriptivo (por ejemplo, api-prod) para que puedas monitorearlo fácilmente:
|
1 2 3 4 5 6 7 |
# Ve al directorio de tu aplicación cd /path/api # Enciende el archivo server.js con PM2 pm2 start server.js --name "api-prod" |
Nota técnica para ES Modules: Como en el código anterior usamos import en lugar de require, asegúrate de que en el archivo package.json de esa misma carpeta tengas la propiedad “type”: “module”. Si no la tienes, puedes iniciar el proceso forzando a Node a interpretar el archivo correctamente ejecutando: pm2 start app.js –name “api-prod” — node –experimental-modules (o simplemente añadiendo “type”: “module” a tu package.json).
2.3 Comandos de Control para Monitorear el Rendimiento
Una vez encendido, puedes usar estos comandos en la terminal para obtener métricas reales:
|
1 2 3 |
pm2 list |
Verás una tabla con el nombre api-prod, el ID del proceso, el estado “online”, el uptime, el uso exacto de CPU y cuánta memoria RAM está consumiendo la API en reposo.
|
1 2 3 |
pm2 monit |
Este comando abre una interfaz visual interactiva en la terminal y muestra las gráficas de consumo y los logs en tiempo real.
2.4 Ver los logs de la API
|
1 2 3 |
pm2 logs api-prod |
Aquí verás impreso el mensaje de éxito: Servidor API corriendo en puerto 3071 y Conectado exitosamente a MongoDB local.
¡Listo! Tu API ya está corriendo de forma real y profesional en segundo plano.
3. Pruebas y Benchmarks
Para medir el rendimiento de la API en condiciones 100% reales de producción dentro de tu VPS, la peor forma es recargar la URL en el navegador de tu computadora. El navegador añade latencias de renderizado visual y no genera carga en el servidor.
La forma estándar en la industria es realizar un Test de Estrés y Carga Sintética. Esto simula a cientos de usuarios reales entrando a tu API al mismo tiempo, obligando a Node.js y a MongoDB a procesar datos bajo presión.
Usaré Autocannon, la cual es una herramienta en Node.js capaz de lanzar cargas exigentes. Al ejecutarse mediante hilos de ejecución ligeros, simula de forma muy precisa conexiones concurrentes reales:
|
1 2 3 |
npm i -g autocannon |
Ahora pasemos a realizar las pruebas.
3.1 Obtener los 50 productos (Listar)
Con este comando probamos el endpoint GET, lo que significa que listamos y traemos los 50 productos masivamente desde MongoDB bajo una carga intensa de usuarios simulados:
|
1 2 3 |
npx autocannon -c 100 -d 10 http://localhost:3071/api/v1/productos |
Explicación de los parámetros:
- -c 100: Simula 100 usuarios concurrentes haciendo peticiones al mismo tiempo de forma ininterrumpida.
- -d 10: Mantiene el test corriendo de forma intensa durante 10 segundos.
Y obtenemos la siguiente respuesta:

Analizando los resultados:
- Peticiones por Segundo (Avg 173.3 Req/Sec): El servidor procesó e imprimió con éxito 1,733 peticiones en total en esos 10 segundos. Significa que la API es capaz de escupir la lista de productos unas 173 veces por segundo de manera sostenida bajo estrés. ¡Es un número muy sólido para empezar!
- La Latencia (Avg 565.03 ms): Aquí vemos el costo del esfuerzo. En promedio, cada usuario tuvo que esperar poco más de medio segundo (565 milisegundos) para recibir la respuesta. El punto máximo de congestión llegó a 1662 ms (1.6 segundos). No es terrible para estar bajo un ataque de 100 conexiones concurrentes, pero nos dice que el hilo único (Single Thread) de Node.js o los tiempos de respuesta de la base de datos se empezaron a acumular en la cola de espera.
Un detalle excelente: se leyó 15.2 MB en total y la desviación estándar (Stdev de 29.82) es baja, lo que significa que el servidor se mantuvo estable. No tuvo caídas drásticas de rendimiento ni picos raros.
3.2 Crear productos masivamente (POST)
Con este comando probamos el endpoint POST, lo que significa que simulamos la creación masiva de nuevos registros, obligando a la API de Express a procesar datos entrantes y a MongoDB a escribir en el disco bajo una carga intensa:
|
1 2 3 |
npx autocannon -c 50 -d 5 -m POST -H "Content-Type: application/json" -b "{\"nombre\":\"Producto de Carga\",\"precio\":25}" http://localhost:3071/api/v1/productos |
Explicación de los parámetros:
- -c 50: Simula 50 usuarios concurrentes enviando peticiones de creación de manera simultánea e ininterrumpida.
- -d 5: Mantiene el test corriendo de forma intensa durante 5 segundos (reducimos el tiempo ya que las operaciones de escritura son más pesadas para el disco).
- -m POST: Especifica que el método HTTP a utilizar será POST en lugar del GET por defecto.
- -H “Content-Type: application/json”: Agrega la cabecera necesaria para indicarle a nuestra API que le estamos enviando datos en formato JSON nativo.
- -b “{\”nombre\”:\”Producto de Carga\”,\”precio\”:25}”: Define el cuerpo (body) de la petición con la estructura del objeto que la base de datos necesita insertar.
Y obtenemos la siguiente respuesta:

Analizando los resultados:
- Peticiones por Segundo (Avg 624.8 Req/Sec): La API logró procesar y meter en la base de datos más de 3,000 productos nuevos en solo 5 segundos. Mantener más de 620 inserciones por segundo en un entorno de un solo hilo (Node.js) con persistencia en disco es un rendimiento fantástico para el VPS.
- La Latencia Real de Escritura (Avg 79.39 ms): Ahora que Express sí tiene que parsear el JSON, Mongoose debe validar el esquema y MongoDB tiene que escribir el registro en el almacenamiento del VPS, el tiempo de respuesta subió a 79.39 milisegundos en promedio.
- Comportamiento en Picos Intensos (Max 196 ms): Incluso en el peor momento del bombardeo con 50 conexiones simultáneas sin pausa, la respuesta más lenta tardó apenas 196 ms (menos de 0.2 segundos). Esto garantiza que la API no se va a colgar ni va a hacer esperar al usuario final bajo ráfagas altas de tráfico.
3.3 Obtener y listar los productos (GET)
Ahora probemos el endpoint GET, lo que significa que realizamos consultas masivas simultáneas para traer el listado completo de productos desde la base de datos de MongoDB bajo una carga intensa de usuarios simulados:
|
1 2 3 |
npx autocannon -c 100 -d 10 http://localhost:3071/api/v1/productos |
Explicación de los parámetros:
- -c 100: Simula 100 usuarios concurrentes haciendo peticiones al mismo tiempo de forma ininterrumpida y sin tiempos de espera entre cada consulta.
- -d 10: Mantiene el test corriendo de forma intensa durante 10 segundos continuos para evaluar la estabilidad del bucle de eventos (Event Loop) de Node.js.
Y obtenemos la siguiente respuesta:

Analizando los resultados:
¿Por qué colapsó el endpoint GET al meterle los miles de productos?
El culpable está en tu archivo server.js, específicamente en esta línea:
|
1 2 3 |
const productos = await Producto.find().sort({ createdAt: -1 }); |
Al no tener paginación, cada una de las 100 conexiones simultáneas le pidió a Express que fuera a MongoDB, trajera los miles de productos creados en el test anterior de golpe, los ordenara por fecha, cargara ese JSON masivo en la memoria RAM de Node.js y lo enviara por la red.
Como el canal de red y el hilo único de Node.js se saturaron transfiriendo archivos JSON gigantescos (¡leyó 20.9 MB con apenas 200 peticiones!), el servidor empezó a dejar peticiones en cola hasta que Autocannon se cansó de esperar y arrojó los timeouts.
El peligro de las consultas masivas sin paginación (Análisis del colapso)
Tras ejecutar el test de estrés POST e inyectar miles de registros en MongoDB, volvimos a lanzar la prueba sobre el endpoint de lectura (GET). El resultado demostró una degradación crítica del rendimiento debido a la falta de límites en la consulta:
- Caída de Req/Sec: El rendimiento cayó drásticamente a un promedio de 2.8 Req/Sec debido a la enorme transferencia de datos en memoria.
- Latencia Crítica: El tiempo de respuesta promedio subió a 6,865.65 ms, provocando que el servidor dejara de responder a tiempo y generando 72 errores de timeout.
- Saturación de Red: Con apenas 200 peticiones completadas, el servidor tuvo que mover 20.9 MB de información, lo que demuestra que transferir colecciones completas sin paginar rompe la escalabilidad del backend.
Solución: Limpieza de la Colección
Para continuar con nuestras pruebas CRUD de manera controlada y mantener los 50 productos base estipulados para el artículo, procederemos a vaciar los registros basura generados por el test de estrés anterior y a reestablecer nuestro listado inicial.
Si estás usando la consola de MongoDB (mongosh), puedes vaciar la colección rápidamente con el siguiente comando:
|
1 2 3 |
db.productos.drop() |
Nota: Tras vaciarla, volví a ejecutar el script de inserción inicial o semilla para restaurar únicamente los 50 productos de prueba.
Repitiendo el Test de Lectura (GET) con 50 Productos
Una vez que restablecimos la base de datos a su estado limpio con exactamente 50 documentos, volvimos a lanzar el test de estrés con Autocannon para medir el rendimiento real de la API en un entorno controlado:
|
1 2 3 |
npx autocannon -c 100 -d 10 http://localhost:3071/api/v1/productos |
Y obtenemos la siguiente respuesta:

Analizando los resultados:
- Estabilización del Rendimiento (Avg 165.3 Req/Sec): Al limitar el peso del payload (los datos transmitidos), el hilo único de Node.js pudo resolver más de 2,000 peticiones en 10 segundos, manteniendo el servidor fluido bajo el bombardeo de 100 usuarios simultáneos.
- Latencia Controlada (Avg 485.89 ms): Aunque el 50% de las peticiones se resolvieron en apenas 294 ms (percentil 50%), vemos que bajo estrés máximo al final del test la latencia llegó a subir (Stdev de 1,080 ms) y se generaron 16 timeouts. Esto es genial, porque demuestra que incluso con 50 registros, meter a 100 usuarios concurrentes sin un sistema de caché (como Redis) o paginación estricta empieza a exigirle el máximo al droplet.
3.4 Actualizar productos de forma masiva (PUT)
Con este comando probamos el endpoint PUT, simulando que múltiples usuarios intentan actualizar los datos de un producto de manera concurrente, forzando a la API a procesar modificaciones en la base de datos:
|
1 2 3 |
npx autocannon -c 50 -d 5 -m PUT -H "Content-Type: application/json" -b "{\"nombre\":\"Producto Actualizado\",\"precio\":35,\"categoria\":\"Electrónica\",\"stock\":20}" http://localhost:3071/api/v1/productos/6a4efafae9e0e60cae8b4d61 |
Explicación de los parámetros:
- -m PUT: Especifica que utilizaremos el método HTTP PUT para realizar la actualización del recurso.
- -b “…”: Envía el objeto JSON con las modificaciones deseadas debidamente escapado para Bash.
Y obtenemos la siguiente respuesta:

Analizando los resultados:
- Velocidad de Procesamiento (Avg 338.4 Req/Sec): Mantener un promedio de casi 340 actualizaciones por segundo en un solo registro bajo el bombardeo de 50 conexiones simultáneas es un rendimiento excelente. Esto demuestra que findByIdAndUpdate() responde de forma sumamente ágil cuando trabaja sobre un único índice directo (el _id).
- Latencia Muy Fluida (Avg 145.56 ms): El tiempo medio que tardó el servidor en recibir el JSON, conectarse a MongoDB, aplicar los cambios en el documento, guardar en disco y retornar la respuesta fue de apenas 145.56 milisegundos. Es una respuesta sumamente rápida para operaciones que modifican el estado de la base de datos.
- Consistencia Máxima (Max 315 ms): Incluso en el pico más alto de estrés de la prueba, la petición más lenta se resolvió en solo 315 ms (menos de un tercio de segundo), manteniendo una desviación estándar bajísima de 42.71 ms. Esto significa que el rendimiento del servidor es predecible y no sufre de caídas o picos extraños durante ráfagas de edición de datos.
3.5 Eliminar productos de forma masiva (DELETE)
Con este comando probamos el endpoint DELETE, simulando un escenario donde múltiples conexiones simultáneas intentan dar de baja un mismo recurso específico en el sistema:
|
1 2 3 |
npx autocannon -c 50 -d 5 -m DELETE http://localhost:3071/api/v1/productos/6a4f0446fa54fe81f68563b1 |
Explicación de los parámetros:
- -m DELETE: Especifica que el método HTTP a utilizar será DELETE, indicándole a Express que la instrucción es remover permanentemente el registro asociado al ID de la URL.
- …/productos/6a4f0446fa54fe81f68563b1: Apunta directamente al identificador único del producto que deseamos eliminar, forzando al backend a ejecutar la acción findByIdAndDelete().
Y obtenemos la siguiente respuesta:

Analizando los resultados:
- Peticiones por Segundo Máximas (Avg 736 Req/Sec): Al no tener que escribir datos complejos en disco de forma repetitiva, el rendimiento subió hasta las 736 peticiones por segundo.
- Latencia Ultra Baja (Avg 67.26 ms): El tiempo de respuesta promedio fue bajísimo comparado con el PUT y el POST. Responder que un recurso ya no existe es una operación sumamente ligera para Express, lo que mantuvo el servidor muy relajado con un pico máximo de apenas 178 ms.
4. Consejos prácticos para optimizar tu API Express y MongoDB
Basándonos en el comportamiento real que observamos durante los tests de estrés, aquí tienes las mejores prácticas que todo desarrollador debe implementar antes de llevar una API de este tipo a producción:
- Implementa Paginación Obligatoria en Lecturas: El colapso del endpoint GET (donde la latencia subió a casi 7 segundos) demostró que transferir colecciones completas rompe el servidor. Usa siempre .skip() y .limit() en Mongoose para segmentar las respuestas en bloques pequeños (por ejemplo, de 20 o 50 registros).
- Aprovecha el Poder de los Índices: Las pruebas PUT y DELETE fueron sumamente rápidas (bajando a ~67 ms en la eliminación) porque MongoDB busca de forma nativa e instantánea a través del índice automático _id. Si vas a buscar productos por campos como sku o categoria, asegúrate de definirlos como índices (index: true) en tu esquema de Mongoose.
- Maneja la Validación en el Cliente y en el Backend: Como vimos en el test POST, omitir campos obligatorios del esquema bloquea la persistencia y genera respuestas de error. Asegúrate de validar los datos entrantes en una capa intermedia (middleware) antes de tocar la base de datos para liberar de carga a MongoDB.
- Considera un Sistema de Caché (como Redis): Si tu endpoint de lectura de productos va a recibir cientos de conexiones concurrentes para mostrar siempre los mismos datos, implementar una capa de caché evitará que Node.js tenga que consultar a MongoDB en cada petición, reduciendo drásticamente los timeouts.
5. Conclusión
Realizar pruebas de estrés con Autocannon nos permite quitar la venda de los ojos y entender cómo se comportará nuestra infraestructura (Node.js, Express y MongoDB) bajo escenarios de tráfico masivo en el mundo real.
Node.js, gracias a su arquitectura basada en un bucle de eventos (Event Loop) no bloqueante, demuestra una capacidad sobresaliente para despachar miles de peticiones HTTP por segundo de manera eficiente. Sin embargo, el verdadero reto no está en el servidor web, sino en la estrategia de persistencia y transferencia de datos. Operaciones de escritura atómicas (POST, PUT) o respuestas ligeras de error rindieron con total fluidez, mientras que las consultas masivas sin control (GET) evidenciaron la necesidad crítica de diseñar código defensivo y optimizado desde el primer día.
Lanzar una API sin antes medir sus límites es una apuesta arriesgada, herramientas ligeras y potentes como Autocannon nos devuelven el control total sobre la escalabilidad de nuestras aplicaciones.

También en las categorías, etiquetas, búsquedas y más.
En versiones anteriores, se veian con alto disparejo.
Seguimos trabajando en mejorar la comunidad.



Seguimos trabajando las 24 horas del día para brindarte la mejor experiencia en la comunidad.
Hemos corregido el problema y ahora la web no muestra esa barra horizontal y se ve en su tamaño natural.
Seguimos trabajando las 24 horas del día, para mejorar la comunidad.
Seguimos trabajando las 24 horas y 365 días del año para mejorar tu experiencia en la comunidad.

Seguimos trabajando para brindarte le mejor experiencia en Nube Colectiva.
Social
Redes Sociales (Developers)
Redes Sociales (Digital)