Optimización de APIs en producción: lo que nadie te dice hasta que el sistema se cae
Hay una diferencia enorme entre un sistema que funciona y uno que aguanta. El primero pasa las pruebas, supera la revisión del cliente y se despliega sin drama. El segundo sobrevive a usuarios reales, picos de tráfico inesperados y archivos CSV de 500,000 filas que alguien sube un martes por la tarde sin avisar.
En junio de 2026, Ali Agboola publicó en Dev.to un análisis técnico detallado sobre dos proyectos de backend que completó durante el programa HNG Internship. No los eligió porque salieran bien. Los eligió porque cada uno le costó un supuesto que no sabía que estaba cargando. Ese es exactamente el tipo de experiencia del que más se aprende — y del que menos se escribe.
En este artículo extraemos las tres lecciones técnicas más valiosas de su trabajo, y las conectamos con decisiones de arquitectura que cualquier equipo de desarrollo en Perú y Latinoamérica puede aplicar hoy.
Lección 1: Los índices individuales no son lo mismo que un índice compuesto
El primer problema que Agboola encontró parecía resuelto antes de empezar. Tenía una tabla de Postgres con un millón de filas y tres columnas indexadas individualmente: gender, country_id y age_group. Lógica razonable: si cada columna tiene su índice, una consulta que filtre por las tres debería ser rápida.
No lo era. Una consulta con los tres filtros tomaba 1.6 segundos. El problema: Postgres no puede combinar tres índices de columna única en una sola búsqueda. Lo que hace en cambio es ejecutar un bitmap scan sobre cada índice por separado y luego intersectar los resultados en memoria. Con un millón de filas, esa intersección es exactamente donde se pierde el tiempo.
La solución fue crear índices compuestos ordenados por selectividad — la columna que más filas elimina va primero:
- @@index([gender, country_id]) para consultas de dos filtros
- @@index([gender, age_group, country_id]) para consultas de tres filtros
El resultado: la misma consulta bajó de 1,680ms a 420ms en un cache miss, y a 11ms cuando el resultado ya estaba en Redis. La lección no es solo técnica — es metodológica: EXPLAIN ANALYZE en Postgres siempre antes de asumir que los índices están haciendo su trabajo. El plan de ejecución real suele ser diferente al que imaginamos.
Lección 2: Las claves de caché son un requisito de correctitud, no de rendimiento
El segundo problema era más silencioso y, por eso, más peligroso. Redis estaba configurado y funcionando. El sistema usaba caché. Pero las claves se generaban directamente desde el objeto de filtros sin normalización previa.
¿El resultado? La consulta gender=male&country_id=NG y la consulta country_id=NG&gender=male generaban claves de caché distintas. Misma pregunta, dos viajes a la base de datos. El caché existía pero no servía para lo que debía servir.
La corrección fue implementar una función de normalización que se ejecuta antes de cualquier otra operación — antes del check de caché y antes de la consulta a la base de datos. El proceso: convertir strings a minúsculas, ordenar las claves alfabéticamente, coercionar numéricos a precisión consistente, eliminar campos undefined, y recién entonces serializar.
Agboola describe un bug particularmente traicionero: había puesto la normalización dentro de la función que construía la clave de caché, pero no en la función que ejecutaba la consulta. El caché veía filtros normalizados; la base de datos recibía los originales. El sistema funcionaba de todas formas porque Postgres era case-insensitive en esa columna. El peor tipo de bug: uno que pasa por accidente.
Para equipos que trabajan con APIs de alto volumen, esto tiene una implicancia directa: una clave de caché inconsistente no solo es un problema de rendimiento — puede devolver resultados para la consulta equivocada si la lógica de invalidación no es perfecta. Es un problema de correctitud disfrazado de lentitud.
Lección 3: El comportamiento ante fallos parciales debe diseñarse, no descubrirse
El tercer desafío fue un endpoint de importación CSV capaz de procesar hasta 500,000 filas sin tumbar el servidor. La aritmética es simple y brutal: insertar fila por fila son 500,000 viajes de red. A 5ms por viaje, son aproximadamente 40 minutos. Cargar el archivo completo en memoria convierte cada upload concurrente en un spike de 50MB en el heap de Node.
La arquitectura que implementó resuelve ambos problemas simultáneamente: el archivo nunca vive en memoria. Multer escribe el upload a disco (/tmp), el servicio abre un stream de lectura sobre ese archivo, y las filas válidas se acumulan en un buffer de 1,000 filas que se vacía con createMany y skipDuplicates: true. 500 viajes en lugar de 500,000.
La decisión de diseño más interesante es la más contraintuitiva: no hay una transacción que envuelva toda la importación. Si el proceso muere en la fila 400,000, las primeras 399,999 filas quedan confirmadas en la base de datos. Re-ejecutar el mismo archivo es seguro porque los duplicados se saltan automáticamente. Hacer rollback de 400,000 filas correctas porque la fila 400,001 falló no ayuda a nadie.
Esta es una decisión deliberada sobre comportamiento ante fallos parciales — el tipo de decisión que debe tomarse en tiempo de diseño, no durante un incidente a las 2am.
¿Cómo aplica esto en empresas de Perú y Latinoamérica?
Los tres problemas que describe Agboola no son exclusivos de internships ni de proyectos académicos. Son exactamente los problemas que aparecen cuando un sistema que funcionaba bien en desarrollo empieza a recibir carga real en producción.
En proyectos de backend para empresas medianas en la región, vemos con frecuencia APIs que responden bien con 10 usuarios concurrentes y se degradan dramáticamente con 100. Las causas más comunes son casi siempre las mismas: consultas sin índices compuestos adecuados, cachés que no normalizan sus claves, y operaciones masivas que no tienen estrategia de fallos parciales.
La diferencia entre un sistema que funciona y uno que escala no es una cuestión de reescribir todo — es una cuestión de aplicar las herramientas correctas en el momento correcto. EXPLAIN ANALYZE antes de asumir que los índices son suficientes. Normalización de claves de caché como paso obligatorio antes de cualquier operación de lectura. Diseño explícito del comportamiento ante fallos en operaciones masivas.
¿Cómo aplica esto en tu empresa?
Si tienes un sistema en producción que sientes que "funciona pero podría ser más rápido", estas son las tres acciones concretas que puedes tomar esta semana:
- Ejecuta EXPLAIN ANALYZE en tus consultas más lentas. No asumas que los índices están funcionando como esperas. El plan de ejecución real suele sorprender.
- Revisa cómo se generan tus claves de caché. Si el orden de los parámetros puede variar, necesitas normalización antes de serializar.
- Define explícitamente qué pasa cuando una operación masiva falla a mitad. ¿Rollback completo o commit parcial? La respuesta correcta depende del contexto, pero debe ser una decisión consciente.
Estas optimizaciones no requieren reescribir la arquitectura. Requieren aplicar criterio de ingeniería a código que ya existe.
Conclusión
Lo más valioso del artículo de Agboola no son los números — aunque pasar de 1,680ms a 11ms es notable. Lo más valioso es la honestidad sobre los supuestos que estaba cargando sin saberlo: que tres índices individuales equivalen a uno compuesto, que un caché sin normalización es suficiente, que el comportamiento ante fallos parciales "se verá después".
En Consultoría-Ti trabajamos con equipos de desarrollo que enfrentan exactamente estos desafíos al escalar sus sistemas. Si tu API funciona pero no aguanta, o si estás diseñando la arquitectura de un nuevo sistema y quieres hacerlo bien desde el inicio, podemos ayudarte a tomar las decisiones correctas antes de que aparezcan en producción.
Escríbenos y conversamos sobre tu proyecto.
Fuentes y Referencias
From Cache Keys to Concurrency — Ali Agboola, Dev.to
✨ Contenido generado con ContentFlow — Consultoría-Ti