31 dic 2025·8 min de lectura

Transmitir grandes respuestas de API: compresión, exportaciones y límites

Aprende a transmitir grandes respuestas de API con gzip/brotli, exportaciones seguras y límites sensatos para que informes grandes se descarguen sin colapsar tu app.

Transmitir grandes respuestas de API: compresión, exportaciones y límites

Por qué las respuestas grandes de la API hacen que las apps fallen

Las respuestas grandes suelen fallar de una forma familiar: el informe funciona en tu portátil con una base de datos pequeña, y luego en producción empieza a agotar el tiempo, a fallar o a devolver un archivo parcial. Los datos son mayores, la red es más lenta y el servidor atiende tráfico real al mismo tiempo.

La mayoría de las apps fallan porque construyen toda la respuesta en memoria antes de enviar nada. Un endpoint “descargar informe” consulta muchas filas, las formatea en JSON o CSV, almacena todo el resultado en un buffer y solo al final lo escribe al cliente. Eso dispara la memoria, activa la recolección de basura y ralentiza todo lo demás.

Los mismos puntos de fallo aparecen una y otra vez:

  • Picos de memoria por almacenar el payload completo (a veces más de una vez por reintentos)
  • Timeouts en el servidor de la app, proxy inverso/load balancer o cliente porque la respuesta tarda demasiado
  • Clientes lentos que leen despacio, mantienen conexiones abiertas y ocupan workers
  • Reintentos que duplican la carga cuando el sistema ya está saturado
  • Respuestas JSON enormes que son costosas de serializar y de parsear para el cliente

El objetivo no es “hacer el archivo más grande posible”. Es entregar respuestas grandes de forma fiable para que una exportación pesada no degrade toda la app para todos los demás.

A menudo puedes arreglar esto sin reescribir la funcionalidad. La mayoría de equipos consiguen estabilidad combinando tres ideas: comprimir cuando ayuda, transmitir datos en fragmentos en vez de acumularlos, y aplicar límites (tamaño, tiempo y filas) que coincidan con lo que realmente necesitan los usuarios.

Si heredaste una app generada por IA donde las exportaciones fallan o la autenticación se rompe a mitad de descarga, normalmente se puede arreglar con cambios puntuales (buffering, consultas sin límites, falta de retropresión). FixMyMess (fixmymess.ai) suele ver estas exportaciones que “funcionan en desarrollo y fallan en producción” y ayuda a convertirlas en descargas seguras para producción.

Compresión, streaming y límites: qué resuelve cada uno

Las respuestas grandes suelen fallar por una de tres razones: el payload es demasiado grande para moverse rápido, demasiado grande para mantenerse en memoria o demasiado costoso de generar. Compresión, streaming y límites atacan cada uno un problema distinto.

La compresión hace el payload más pequeño en la red. El servidor envía menos bytes y el cliente descarga más rápido. Esto ayuda cuando el contenido es mayoritariamente texto (JSON, CSV). Ayuda menos cuando los datos ya están comprimidos (imágenes, PDFs, ZIP). La compresión tampoco arregla el error principal de construir primero una cadena de 200 MB en memoria.

El streaming cambia cómo entregas la respuesta. En vez de construir toda la exportación y luego enviarla, la envías en pequeños fragmentos a medida que la produces. Esta es la herramienta principal cuando necesitas enviar millones de filas sin quedarte sin RAM. El streaming mantiene la memoria estable, pero no hace que la respuesta sea automáticamente más pequeña ni más barata de calcular.

Las exportaciones son un caso especial. Enviar una respuesta JSON de 200 MB no es lo mismo que ofrecer una descarga. Una descarga puede transmitirse como una respuesta tipo archivo (CSV/JSONL) y procesarse a medida que llega. Una respuesta JSON masiva a menudo obliga a los clientes a parsearlo todo a la vez, lo que puede congelar la UI o colapsar apps móviles.

Los límites son la red de seguridad. Evitan que peticiones en el peor caso tumben tu app cuando alguien selecciona “todo el tiempo” y “todos los clientes”. Buenos límites suelen incluir número máximo de filas o bytes, tiempo máximo de solicitud y límites de tasa en endpoints de exportación. Muchos equipos también ponen valores por defecto razonables como un rango de fechas limitado y exigen un trabajo asíncrono/en background para informes muy grandes.

Los equipos que arreglan exportaciones rotas generadas por IA suelen necesitar los tres: compresión para velocidad, streaming para seguridad de memoria y límites para que una petición no perjudique a todos.

Elegir gzip o brotli sin adivinar

La compresión significa que el servidor empaqueta la respuesta en menos bytes antes de enviarla. El cliente la descomprime automáticamente. Para payloads JSON grandes y exportaciones, esto puede marcar la diferencia entre una descarga rápida y una petición que agota el tiempo.

Gzip es la opción más antigua y ampliamente soportada. Brotli es más nuevo y a menudo reduce un poco más el tamaño del texto, especialmente JSON y HTML. Ambos funcionan mejor con texto. Ninguno ayuda mucho si tu respuesta ya está comprimida.

Cómo deciden el cliente y el servidor

Los clientes le dicen al servidor qué pueden decodificar usando la cabecera Accept-Encoding de la petición (por ejemplo: br, gzip). El servidor debería elegir uno de esos esquemas y fijar Content-Encoding en la respuesta. Si falta la cabecera, envía la respuesta sin comprimir.

Una regla práctica: elige el mejor encoding que el cliente dice soportar, con una alternativa segura.

  • Si se acepta br, usa Brotli para respuestas de texto.
  • Si no, si se acepta gzip, usa gzip.
  • Si ninguno se acepta, envía bytes sin comprimir.

Cuándo gzip es más seguro y cuándo tiene sentido brotli

Elige gzip por defecto cuando necesitas máxima compatibilidad (clientes antiguos, proxies inusuales o entornos mixtos). Elige Brotli cuando la mayor parte del tráfico son navegadores modernos o tus propios clientes controlados, y te importa ahorrar algo más de ancho de banda.

Ten en cuenta la compensación de CPU. Respuestas más pequeñas pueden costar más trabajo de servidor. Brotli suele usar más CPU que gzip en ajustes similares. Si tu servidor ya está ocupado generando informes, la compresión puede empujarlo al límite. Un enfoque común: gzip para la mayoría de JSON de API, Brotli para endpoints orientados al navegador y niveles de compresión más bajos para descargas muy grandes.

También evita comprimir formatos ya comprimidos (archivos ZIP, PDFs, PNG/JPEG, muchos tipos de audio/video). Gastas CPU y a veces el archivo queda incluso más grande.

Si heredas un backend generado por IA, una buena vía rápida es gzip para respuestas de texto más límites claros de tamaño. Añade Brotli solo donde puedas demostrar que ayuda.

Cómo añadir compresión de forma segura

La compresión suele ser la ganancia más rápida, pero puede crear bugs confusos si las cabeceras están mal o los mismos datos se comprimen dos veces.

Empieza con una regla simple: comprime solo cuando ayuda. Si la respuesta es pequeña, comprimir puede gastar CPU y añadir latencia. Un umbral práctico es alrededor de 1-2 KB para JSON y 4-8 KB para CSV o texto plano. Por debajo de eso, envía tal cual.

La compresión funciona mejor para contenido basado en texto como JSON, CSV, HTML y logs. Suele hacer poco por imágenes, PDFs o archivos ya comprimidos (ZIP). Para esos, evita la compresión.

Fija cabeceras para que navegadores, proxies y caches se comporten:

  • Content-Encoding: gzip o br para indicar al cliente qué usaste
  • Vary: Accept-Encoding para que los caches no mezclen versiones comprimidas y sin comprimir
  • Un Content-Type correcto (por ejemplo application/json o text/csv) para que los clientes lo parseen bien
  • Si haces streaming, evita poner un Content-Length que no puedas garantizar

Evita la doble compresión. Esto ocurre cuando tu app comprime respuestas y un proxy inverso o middleware del framework vuelve a comprimir. Elige un único lugar para hacerlo y confirma revisando las cabeceras de respuesta y haciendo una prueba rápida para asegurarte de que los bytes coinciden con Content-Encoding.

Pruébalo como un mini benchmark: compara tamaño de respuesta, tiempo total y CPU antes y después. También prueba redes lentas y “cancelar a mitad de descarga”, porque la compresión mal configurada suele mostrarse como descargas rotas o peticiones colgadas.

Transmitir exportaciones para que no exploten la memoria

La manera más segura de manejar exportaciones grandes es no construir nunca todo el archivo en memoria. Genera una fila (o un pequeño lote) y envíala al cliente inmediatamente. Bien hecho, el servidor hace trabajo constante y la descarga crece con el tiempo.

Para exportaciones, los formatos tipo archivo suelen ser más fáciles que “un enorme array JSON” porque puedes escribir línea a línea. CSV funciona bien para hojas de cálculo. NDJSON (un objeto JSON por línea) funciona bien para procesamiento por máquinas y datos estilo log.

Cuando transmites respuestas grandes, las conexiones lentas importan. Si un usuario descarga por una red móvil débil, el servidor no debe almacenar toda la exportación mientras espera enviarla. Usa escrituras conscientes de backpressure (la mayoría de frameworks web lo soportan) para producir datos solo tan rápido como puedan entregarse.

Las descargas largas también necesitan timeouts amigables. Mantén la conexión viva con salida periódica y establece timeouts de servidor/proxy lo bastante altos para los tamaños de informe esperados. Si tienes un proxy inverso delante, confirma que permite respuestas de larga duración o cortará la exportación a mitad.

El streaming cambia el manejo de errores. Una vez que empiezas a enviar el archivo, no puedes cambiar a una respuesta JSON de error bonita. Planifica esto antes de lanzar:

  • Valida entradas y permisos antes de enviar el primer byte.
  • Escribe un encabezado pronto (columnas CSV o una línea de metadatos para NDJSON).
  • Si algo falla a mitad de stream, regístralo, detente limpiamente y deja claro que el archivo está incompleto.

Un error común de “funciona en dev” es construir arrays con cientos de miles de filas. Pasar a exportaciones en streaming suele eliminar inmediatamente el pico de memoria y mantener la app receptiva mientras la descarga avanza.

Paso a paso: implementar una descarga segura en streaming

Resolve big response timeouts
Arreglamos la lógica rota, los reintentos y las consultas lentas que provocan timeouts en respuestas grandes.

Trata una descarga como una tubería en vivo, no un gran objeto que construyes en memoria y devuelves al final.

Empieza eligiendo el formato de exportación según cómo lo usen las personas. CSV es excelente para hojas de cálculo. NDJSON es mejor cuando otro sistema lo va a leer. Un bundle ZIP puede ayudar cuando envías varios archivos, pero no uses ZIP solo para ocultar problemas de rendimiento.

Luego, haz que el trabajo sea incremental. En vez de una consulta enorme, lee filas por páginas (o mediante un cursor) y recorre hasta que no haya más datos. Tu app solo debe mantener una pequeña porción a la vez.

Una secuencia simple que evita la mayoría de fallos “el informe mató al servidor”:

  • Configura cabeceras desde el inicio (tipo + nombre de archivo de la descarga) y empieza la respuesta.
  • Recupera datos por páginas y convierte cada página en líneas de salida.
  • Escribe fragmentos y vacía el buffer con frecuencia (no construyas una cadena gigante).
  • Comprime sobre la marcha cuando ayuda (gzip en streaming es ampliamente soportado).
  • Detén el trabajo cuando el cliente se desconecte o el usuario cancele.

La compresión es el bonus, no la base. Reduce ancho de banda, pero el streaming es lo que mantiene la memoria estable. Para CSV y NDJSON, gzip suele dar una gran ventaja, siempre que comprimas mientras escribes y no después de generar todo el archivo.

Valida con un conjunto de datos realmente grande. Una prueba de 1.000 filas puede parecer perfecta mientras una exportación de 5 millones de filas se queda sin memoria, agota el tiempo o genera un archivo truncado.

Ejemplo: una exportación “Transacciones Mensuales” falla en producción porque carga todas las filas y luego las junta en una sola cadena CSV. Cambiar a un bucle por páginas y escrituras por fragmentos lo arregla sin cambiar lo que recibe el usuario.

Aplicar límites de tamaño y tiempo que los usuarios acepten

Si quieres respuestas grandes sin caídas aleatorias, necesitas límites que protejan el servidor y sigan pareciendo justos para los usuarios. El truco es hacer los límites previsibles, visibles en el comportamiento y acompañados de un siguiente paso obvio.

Empieza con dos topes duros: filas máximas y bytes máximos. Los topes por filas evitan que consultas lentas se ejecuten indefinidamente. Los topes por bytes previenen respuestas “exitosas” que sobrecargan proxies o buffers. Cuando una exportación llega a un tope, devuelve un mensaje claro que diga qué pasó y qué cambiar (por ejemplo, “Exportación limitada a 100.000 filas. Reduce el rango de fechas o aplica un filtro.”).

Pon guardarraíles en la propia consulta para que la base de datos haga menos trabajo. Guardarraíles comunes incluyen un rango de fechas por defecto (31 o 90 días), requerir al menos un filtro de afinamiento para informes “todos los clientes” y un tamaño máximo de página aunque el cliente pida más. Si permites ordenación o filtrado, manten una allow-list y asegúrate de que la base de datos lo pueda soportar.

Los límites de tiempo deben existir en varias capas: timeout de sentencia en la base de datos, timeout de petición en el servidor y un plazo a nivel de aplicación para generar exportaciones. Cuando cortas trabajo, hazlo limpiamente. Devuelve un error específico que diga a los usuarios cómo tener éxito la próxima vez, no un 500 genérico.

La limitación de tasa es la otra mitad de “límites vivibles”. Un usuario que descarga repetidamente un informe enorme no debería dejar sin recursos al resto. Controla endpoints costosos por usuario y por organización, y considera límites separados para peticiones interactivas versus exportaciones.

Finalmente, registra las peticiones que rozan los límites (filas, bytes, tiempo, filtros usados) y alerta cuando se agrupan. Si muchos usuarios alcanzan un tope de 90 días, eso indica añadir un informe resumen o una opción de exportación asíncrona.

Controles de seguridad para exportaciones y respuestas grandes

Make exports safe in production
Convertimos tu función de exportación generada por IA en una descarga en streaming segura para producción.

Las exportaciones grandes fallan de dos maneras: la app se cae o filtran datos silenciosamente. Trata las exportaciones como una función separada con sus propias reglas de seguridad.

Empieza por la autorización. Un fallo común es un endpoint de exportación que comprueba “¿está el usuario logueado?” pero olvida “¿puede ver estas filas?”. Reutiliza las mismas comprobaciones de permisos que el informe en pantalla y aplícalas en el servidor antes de escribir cualquier dato.

Las exportaciones CSV tienen un riesgo especial: inyección en CSV. Si un campo controlado por el usuario empieza con caracteres como =, +, - o @, abrir el archivo en una hoja de cálculo puede ejecutar una fórmula. La solución es sencilla: escapar o anteponer un carácter seguro (por ejemplo, una apóstrofe inicial) a los valores riesgosos que vienen de usuarios.

Los fallos en exportaciones también pueden derramar secretos. Cuando un job agota el tiempo, puede tentar registrar la consulta completa, cabeceras o cuerpo de la petición. Eso puede exponer claves API, tokens de autenticación o datos personales en los logs. Prefiere un ID interno de exportación más un código de error corto y evita valores sensibles en los stack traces.

Los filtros flexibles del informe son otra trampa. “Ordenar por cualquier columna” o “filtrar con una cadena de consulta cruda” puede convertirse en inyección SQL si construyes SQL concatenando strings. Usa consultas parametrizadas y allow-lists para campos ordenables y filtrables.

También protege las exportaciones de larga duración contra abuso. Un pequeño conjunto de guardarraíles ayuda mucho:

  • Revisa el token de usuario (o la sesión) cuando comienza la exportación, no solo cuando se encoló
  • Limita las exportaciones por usuario/workspace
  • Pon un tope duro en filas o tiempo y devuelve un mensaje claro cuando se alcanza
  • Registra quién exportó qué y cuándo para auditoría

Estas comprobaciones son fáciles de pasar por alto cuando te concentras en “hacer que descargue”. El objetivo es una descarga fiable y segura en producción.

Errores comunes que provocan descargas rotas

Las descargas rotas suelen ocurrir porque el servidor intenta ser “útil” en el sitio equivocado. Una prueba rápida con datos pequeños funciona, luego un informe real en producción hace que la app se congele, agote el tiempo o devuelva un archivo que no se abre.

Una trampa sencilla es comprimirlo todo. Comprimir una respuesta JSON de 2 KB puede costar más CPU de la que ahorras, especialmente bajo carga. La compresión brilla cuando las respuestas son grandes y repetitivas (exportaciones, listas largas, logs). Para respuestas pequeñas, evítala o aplica un tamaño mínimo.

Otro fallo común es construir toda la exportación en memoria antes de enviarla. Parece más simple crear una gran cadena o buffer, pero escala mal. Un CSV de 200 MB puede crecer mucho más en memoria durante el formateo, y unos pocos usuarios haciendo esto a la vez pueden tumbar el proceso.

Otros errores frecuentes:

  • Llamar “streaming” a algo que aún genera el CSV completo primero y luego lo escribe
  • Transmitir JSON de forma que queda inválido (corchetes faltantes, comas finales, objetos parciales)
  • Ignorar timeouts del cliente, proxy inverso o load balancer (la exportación corre pero la conexión ya está muerta)
  • Probar solo en una red local rápida con datos pequeños y desplegar sin pruebas en red lenta o con tamaños reales
  • Alcanzar límites (tamaño/tiempo) sin mensaje claro, de modo que los usuarios solo ven una descarga fallida

El streaming de JSON merece cuidado especial. Si necesitas JSON estricto, transmite un array bien formado y maneja las comas con cuidado. Si los clientes pueden aceptarlo, elige un formato pensado para streaming como JSON Lines/NDJSON.

Cuando se alcanzan límites, di al usuario qué pasó y qué hacer a continuación (reducir filtros, rango de fechas más pequeño o solicitar una exportación asíncrona).

Un ejemplo realista: arreglar una exportación que falla

Un fundador hace clic en “Informe de ventas mensuales” y espera. La pestaña del navegador gira, la app se vuelve lenta para todos y tras un minuto la descarga falla. En el servidor, el endpoint del informe estaba construyendo todo el CSV en memoria antes de enviar nada. Un mes grande (o unas columnas extras) empujaron la memoria al límite y el proceso se reinició.

La solución no fue “hacer el servidor más grande”. Fue cambiar cómo se produce y entrega la exportación para que maneje conjuntos de datos grandes de forma predecible.

Qué cambió:

  • El servidor escribe filas CSV según las lee de la base de datos, en vez de acumularlas en una gran cadena.
  • Se habilitó compresión gzip para la descarga para que el archivo fuera más pequeño en la red.
  • Se añadió un tope duro (por ejemplo, 31 días por petición) con un mensaje claro si el usuario pide más.
  • Se aplicaron timeout y límite máximo de filas para que una petición no pueda acapararlo todo.

La experiencia del usuario mejora de inmediato. La descarga comienza en uno o dos segundos porque el servidor puede enviar cabeceras y los primeros bytes al momento. El archivo a menudo termina antes porque es más pequeño, y las fallas disminuyen porque el servidor ya no intenta mantener todo en memoria. Si el usuario necesita un rango más largo, la UI puede guiarle para ejecutar varias exportaciones.

Para el equipo, la mayor ganancia es estabilidad. La memoria se mantiene estable, los picos de CPU son menores y los tickets de soporte como “el informe congeló la app” desaparecen. Esto es el tipo de trabajo que FixMyMess suele hacer cuando prototipos generados por IA fallan en producción: mover exportaciones a streaming, añadir compresión segura y poner límites para que una sola exportación no tumbe la app.

Lista rápida antes de desplegar

Untangle spaghetti architecture
Refactorizamos código generado por IA enredado para que las exportaciones sigan siendo rápidas al crecer los datos.

Prueba el peor caso, no la vía feliz. Elige el informe más grande que los usuarios puedan pedir y ejecútalo de extremo a extremo tal y como lo harán (mismos filtros, mismos roles y, a ser posible, un dispositivo real). Ahí es donde las descargas que “funcionan en mi máquina” suelen romperse.

Checklist:

  • Ejecuta la exportación más grande y confirma que termina con éxito (sin 500s, sin archivos parciales, sin “error de red” tras un par de minutos).
  • Observa la memoria del servidor durante la exportación. Debe mantenerse mayormente plana. Una subida lenta y constante suele indicar buffering en vez de streaming.
  • Asegúrate de que los límites sean visibles donde los usuarios eligen el informe: rango máximo de fechas, topes de filas y cualquier timeout.
  • Verifica autorización con roles reales (admin, usuario estándar, roles restringidos). Confirma que no puedes exportar datos que no puedes ver.
  • Revisa logs después de una ejecución grande: tamaño de respuesta, si se usó compresión, tiempo de generación y si la petición alcanzó un límite.

Si heredaste una exportación generada por IA que sigue fallando, la solución más rápida suele ser una auditoría corta para encontrar dónde ocurre el buffering, luego añadir streaming y límites duros.

Próximos pasos si tu app ya se está cayendo

Si la app se cae cuando los usuarios ejecutan informes grandes, trátalo como un incidente: primero para la hemorragia y luego mejora la experiencia. Afinar compresión antes de tener guardarraíles suele ser perder tiempo.

Un orden sensato de trabajo:

  • Añade límites duros (filas máximas, bytes máximos, tiempo máximo) y devuelve un error claro cuando se alcanzan.
  • Cambia las exportaciones a streaming para que el servidor nunca cargue todo el archivo en memoria.
  • Ajusta la compresión cuando lo básico ya sea estable y solo donde aporte beneficios.

Una vez en marcha los límites, puedes soportar descargas grandes sin tumbar el proceso. El cambio clave es evitar buffering: no construyas el JSON/CSV completo en un array o string y no registres payloads completos en errores.

Haz un plan rápido de pruebas del peor caso antes de tocar producción:

  • El informe más grande que los usuarios realmente ejecutan (o la tabla más grande en prod)
  • Una red cliente lenta (throttled) durante la descarga
  • Dos o tres exportaciones concurrentes desde distintos usuarios
  • Una descarga cancelada a mitad
  • Una petición que alcanza el límite (verifica el mensaje y que el servidor se mantiene sano)

Si tu base de código fue generada por herramientas como Lovable, Bolt, v0, Cursor o Replit, estos bugs suelen esconderse en unos pocos sitios: wrappers de auth que reintentan para siempre, manejo de errores que vuelca respuestas completas en logs y utilidades helper que llaman a toString() o json() demasiado pronto (forzando buffering completo).

Una remediación rápida suele ser: diagnóstico (encuentra dónde sube la memoria y dónde ocurre el buffering), correcciones puntuales (límites + streaming + errores más seguros), verificación (pruebas de carga y comprobación de integridad de exportaciones) y preparación del despliegue (timeouts, dimensionado de workers y monitorización). Si quieres una segunda opinión, FixMyMess en fixmymess.ai puede ejecutar una auditoría de código gratuita para localizar puntos de fallo en exportaciones, huecos de seguridad y problemas de rendimiento, y luego ayudar a desplegar una solución en 48-72 horas.