Saltar al contenido

Nginx gzip y Brotli: compresión, caché del navegador y bloquear bots de IA

Publicado el 07-10-20268 min de lecturaPor la redacción de SmoothSeen

En nginx, gzip se activa con gzip on, gzip_types para los tipos de texto y gzip_vary on. Brotli no viene en los paquetes de nginx.org: necesita el módulo ngx_brotli, que hay que compilar aparte. La caché del navegador se fija por location con expires o add_header Cache-Control, y los bots de IA de entrenamiento se vetan con map y return 403.

Lo esencial

  • Con gzip on, gzip_types y gzip_vary, un HTML de 6.143 bytes viajó en 413 bytes en nuestra prueba con nginx 1.30.5 y 1.31.6.
  • Los paquetes de nginx.org no incluyen Brotli. El módulo ngx_brotli es un proyecto de Google en GitHub que hay que compilar; NGINX Plus lo ofrece como paquete.
  • Una expresión regular con llaves en un location tiene que ir entre comillas; sin ellas, nginx -t falla con «unknown directive».
  • Un add_header de caché en un location borra las cabeceras de seguridad del server; add_header_inherit merge en el server lo evita desde nginx 1.29.3.
  • Vetar GPTBot o ClaudeBot con map y return 403 no te saca de ChatGPT ni de Claude; vetar OAI-SearchBot o Claude-SearchBot, sí.

Para comprobarlo en tu web: Auditoría SEO

En esta página

Nginx decide aquí tres cosas: si una respuesta viaja comprimida, cuánto tiempo puede guardarla el navegador y a quién responde con un 403. No controla lo que haga un CDN que tengas delante, ni puede distinguir a un bot auténtico de uno que se hace pasar por él. Los fragmentos se han probado el 07-10-2026 con nginx 1.30.5 (estable) y nginx 1.31.6 (principal), en sus imágenes oficiales de Docker, con curl desde otro contenedor; la excepción es Brotli, y se explica por qué. HTTPS y cabeceras de seguridad están en la guía de nginx para HTTPS, HSTS y cabeceras.

Paso 1: activar gzip

Estas líneas van en el bloque http, o en un fichero de conf.d/, que la configuración de serie incluye dentro de http:

gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;
Directiva
gzip
Qué hace
Activa la compresión
Valor por defecto1
off
Directiva
gzip_vary
Qué hace
Añade Vary: Accept-Encoding, para que un proxy no sirva la versión comprimida a quien no la entiende
Valor por defecto1
off
Directiva
gzip_min_length
Qué hace
No comprime respuestas más pequeñas que esto, en bytes
Valor por defecto1
20
Directiva
gzip_types
Qué hace
Tipos que se comprimen, además de text/html, que siempre se comprime
Valor por defecto1
text/html
Directiva
gzip_comp_level
Qué hace
Nivel de 1 (rápido) a 9 (más compresión)
Valor por defecto1
1
Directiva
gzip_proxied
Qué hace
Si comprime peticiones que llegan a través de un proxy
Valor por defecto1
off

Bytes transferidos en la prueba, idénticos en las dos versiones:

Fichero
HTML
Sin comprimir
6.143
gzip
413
Fichero
CSS
Sin comprimir
4.710
gzip
277
Fichero
JavaScript
Sin comprimir
5.670
gzip
297
Fichero
Imagen WebP
Sin comprimir
4.000
gzip
4.000

Tres detalles prácticos:

  • No pongas text/html en gzip_types: ya se comprime siempre.
  • Pon los dos tipos de JavaScript. El mime.types de nginx sirve los .js como application/javascript, pero el de Apache usa text/javascript; si tu nginx hace de proxy de otro servidor, puede llegarte cualquiera de los dos.
  • Detrás de un CDN, mira gzip_proxied. Nginx considera que una petición llega por un proxy si trae la cabecera Via, y con el valor por defecto off no la comprime1.

La documentación de nginx avisa también de que las respuestas comprimidas por SSL/TLS pueden ser vulnerables a los ataques BREACH1: si tu web maneja tokens en páginas dinámicas, revísalo.

¿Y Brotli?

Nginx no trae Brotli de serie. La página de paquetes de nginx.org enumera sus módulos dinámicos (geoip, image-filter, njs, perl, xslt, otel y acme) y Brotli no está entre ellos2. En la imagen oficial de Docker tampoco hay ningún módulo Brotli en /usr/lib/nginx/modules/: lo comprobamos.

Hay dos vías:

  1. ngx_brotli, un proyecto de Google en GitHub. Se compila como módulo dinámico contra tu versión exacta de nginx (./configure --with-compat --add-dynamic-module=...) y se carga con load_module3.
  2. NGINX Plus, la versión comercial, que lo ofrece como paquete nginx-plus-module-brotli4.

Este bloque, con los nombres de módulo y directivas que documenta el propio proyecto, no lo hemos probado, porque las imágenes oficiales no incluyen el módulo:

# En nginx.conf, fuera de cualquier bloque (contexto principal)
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

# En el bloque http
brotli on;
brotli_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;

Si compilar un módulo de terceros y repetirlo en cada actualización no te compensa, gzip bien configurado ya cubre lo esencial.

Ficheros precomprimidos con gzip_static

gzip_static sirve el fichero .gz que tengas al lado del original, en vez de comprimir en cada petición. El módulo no se compila por defecto, pero los paquetes oficiales lo traen (--with-http_gzip_static_module aparece en nginx -V)5.

# En el bloque http, junto a las líneas de gzip
gzip_static on;

En la prueba, con app.3f9a1c2b.js.gz creado con gzip -9 -k, nginx devolvió Content-Encoding: gzip y los 318 bytes del fichero precomprimido, con el Cache-Control de su location intacto; sin Accept-Encoding, sirvió el original de 5.670 bytes. Donde no hay .gz, sigue comprimiendo al vuelo. La documentación pide que el original y el .gz tengan la misma fecha de modificación5; gzip -k la conserva. La ventaja es el tiempo de CPU, no tanto los bytes: el mismo fichero comprimido al vuelo ocupó 297.

Paso 2: la caché del navegador por tipo de fichero

La regla de web.dev: las URL con versión o huella en el nombre pueden guardarse un año (max-age=31536000), y las que no la llevan, como el HTML, deben ir con no-cache, que significa «revalida antes de usar»6. MDN define immutable como la promesa de que la respuesta no cambiará mientras esté fresca7.

# HTML y todo lo demás: se guarda, pero se revalida
location / {
    add_header Cache-Control "no-cache";
}

# Ficheros con huella en el nombre: un año, immutable
# (la expresión lleva llaves: tiene que ir entre comillas)
location ~* "\.[0-9a-f]{8,}\.(css|js|mjs|woff2|webp|avif|png|jpe?g|svg)$" {
    add_header Cache-Control "max-age=31536000, immutable";
}

# CSS y JS sin huella: una semana
location ~* \.(css|js|mjs)$ {
    expires 7d;
}

# Imágenes y fuentes sin huella: un mes
location ~* \.(webp|avif|png|jpe?g|gif|svg|woff2)$ {
    expires 30d;
}
Petición
/
Cache-Control obtenido
no-cache
Petición
/assets/estilos.css
Cache-Control obtenido
max-age=604800 y Expires
Petición
/assets/app.3f9a1c2b.js
Cache-Control obtenido
max-age=31536000, immutable
Petición
/assets/foto.webp
Cache-Control obtenido
max-age=2592000 y Expires

Tres cosas que aprendimos en la prueba:

  1. Las llaves rompen la configuración si no hay comillas. La primera versión, con la expresión sin comillas, hizo fallar nginx -t con unknown directive "8,}\.(css|js...: nginx interpretó la llave como el inicio del bloque.
  2. El orden importa. Nginx usa la primera expresión regular que encaja, en el orden en que aparecen; por eso la de los ficheros con huella va antes que la genérica de CSS y JS.
  3. expires pone Expires y Cache-Control: max-age a la vez8, y no cuenta como add_header a efectos de herencia: el location con expires siguió recibiendo las cabeceras de seguridad del server.

Y la trampa de siempre: un add_header Cache-Control en un location borra las cabeceras de seguridad que pusiste en el server, porque add_header solo se hereda si el nivel actual no tiene ninguno8. Desde nginx 1.29.3 basta con una línea en el server:

add_header_inherit merge;

Con ella, en las dos versiones probadas, /, el fichero con huella y el CSS llevaron su Cache-Control y además X-Content-Type-Options y Referrer-Policy. Sin ella, los dos locations con add_header las perdieron.

Paso 3: bots de IA, separando búsqueda y entrenamiento

Los rastreadores de búsqueda deciden si ChatGPT, Claude o Perplexity pueden citarte: OAI-SearchBot (si lo vetas, no sales en las respuestas de búsqueda de ChatGPT9), Claude-SearchBot y Claude-User (vetarlos puede reducir tu visibilidad en Claude10) y PerplexityBot, que no se usa para entrenar modelos11. Los de entrenamiento, como GPTBot o ClaudeBot, solo deciden si tu contenido se usa para entrenar; vetarlos es una decisión editorial que no te saca de las respuestas910.

Este veto, en contexto http (map solo se admite ahí12), deja fuera a tres rastreadores de entrenamiento y a CCBot, el de Common Crawl, y deja siempre abierto robots.txt:

map $http_user_agent $bot_entrenamiento {
    default 0;
    "~*(GPTBot|ClaudeBot|CCBot|meta-externalagent)" 1;
}

map "$bot_entrenamiento:$uri" $vetar_bot {
    default          0;
    "1:/robots.txt"  0;
    "~^1:"           1;
}

Y en el server:

if ($vetar_bot) {
    return 403;
}

El segundo map funciona porque nginx comprueba primero las cadenas exactas y después las expresiones regulares12: 1:/robots.txt gana a ~^1:. Resultado en las dos versiones: los cuatro vetados recibieron 403 en /blog/ y 200 en /robots.txt; OAI-SearchBot, ChatGPT-User, Claude-SearchBot, PerplexityBot, Googlebot y un Chrome normal, 200 en las dos. Anthropic explica que bloquear a sus bots de forma que no puedan leer robots.txt puede no garantizar la exclusión10: por eso la excepción.

Los límites de cualquier veto por user-agent:

  • Se puede falsificar. Common Crawl dice que conoce rastreadores que se hacen pasar por CCBot y publica sus rangos de IP para comprobarlo13; OpenAI y Perplexity también publican los suyos.
  • Google-Extended no es un user-agent. Es una marca de robots.txt sin user-agent propio, que no afecta a la Búsqueda de Google14. Con map no se ve: va en robots.txt.
  • ChatGPT-User y Perplexity-User pueden no seguir robots.txt, según sus dueños911. Vetarlos en el servidor te saca de las consultas que hacen los usuarios.

Para los bots que respetan robots.txt, robots.txt es la vía más clara y la que cualquiera puede leer; el map queda como refuerzo:

User-agent: GPTBot
User-agent: ClaudeBot
User-agent: CCBot
User-agent: meta-externalagent
User-agent: Google-Extended
Disallow: /

User-agent: *
Allow: /

Cómo comprobarlo

  1. Sintaxis. nginx -t antes de cada recarga.
  2. Compresión. curl -sI -H "Accept-Encoding: gzip" https://www.example.com/ debe mostrar Content-Encoding: gzip y Vary: Accept-Encoding. Repite con un .js y un .css.
  3. Caché y cabeceras. curl -sI sobre la portada, un CSS y un fichero con huella: compara cada Cache-Control con la tabla y comprueba que las cabeceras de seguridad siguen ahí.
  4. Bots. curl -sI -A "GPTBot/1.4" https://www.example.com/ debe dar 403, y con -A "OAI-SearchBot/1.4", 200.

Qué hace SmoothSeen con esto

SmoothSeen comprueba dentro de Posicionamiento web si tu web sirve el texto comprimido con gzip o Brotli. En Posicionamiento IA lee tu robots.txt separando los rastreadores de búsqueda de los de entrenamiento, sin contar como fallo vetar a los de entrenamiento, y pide la página con el user-agent de cada rastreador de IA, de un navegador y de Googlebot para detectar si el servidor responde 403 a alguno.

Qué hacer esta semana

Pide un .js de tu web con curl -sI -H "Accept-Encoding: gzip": si no sale comprimido, revisa gzip_types. Después pide la portada con -A "OAI-SearchBot/1.4" y confirma que recibe un 200. Para revisar compresión, robots.txt y vetos del servidor de una vez, analiza tu web con SmoothSeen.

Preguntas frecuentes

¿Por qué nginx no comprime mis ficheros JavaScript?

Lo habitual es que el tipo MIME con el que salen no esté en gzip_types. El mime.types de nginx usa application/javascript, pero si nginx hace de proxy de otra aplicación puede recibir text/javascript. Mira la cabecera Content-Type con curl -sI y añade a la lista exactamente ese valor. Comprueba también que el fichero supere gzip_min_length.

¿Tengo que recompilar nginx para usar Brotli?

Con los paquetes de nginx.org, sí: hay que compilar el módulo ngx_brotli contra tu versión exacta de nginx y cargarlo con load_module, y repetirlo en cada actualización. Algunas distribuciones lo empaquetan para su propio nginx, y NGINX Plus lo ofrece como paquete. Si nada de eso encaja, gzip sigue siendo una mejora grande.

¿Bloquear GPTBot me saca de ChatGPT?

No. OpenAI separa GPTBot, que usa para entrenar modelos, de OAI-SearchBot, que decide si tu web aparece en la búsqueda de ChatGPT, y cada uno se configura por separado. Puedes vetar el entrenamiento y seguir saliendo en las respuestas con búsqueda. Lo que sí te saca es vetar a OAI-SearchBot.

¿Por qué no se ven mis cambios de CSS después de activar la caché?

Porque el navegador usa su copia guardada hasta que caduca. Pasa cuando das un año, o immutable, a un fichero cuyo nombre no cambia al cambiar su contenido. La solución es que tu herramienta de publicación añada una huella o un número de versión al nombre, o limitar esos ficheros a una semana con expires 7d.

Fuentes

  1. 1Module ngx_http_gzip_module, nginx.org, consultada el 07-10-2026.
  2. 2nginx: Linux packages, nginx.org, consultada el 07-10-2026.
  3. 3google/ngx_brotli, Google, GitHub, consultada el 07-10-2026.
  4. 4Brotli (dynamic module), F5 NGINX, consultada el 07-10-2026.
  5. 5Module ngx_http_gzip_static_module, nginx.org, consultada el 07-10-2026.
  6. 6Prevent unnecessary network requests with the HTTP Cache, web.dev (Google), consultada el 07-10-2026.
  7. 7Cache-Control, MDN Web Docs, actualizada el 17-09-2026.
  8. 8Module ngx_http_headers_module, nginx.org, consultada el 07-10-2026.
  9. 9Overview of OpenAI Crawlers, OpenAI, consultada el 07-10-2026.
  10. 10Does Anthropic crawl data from the web, and how can site owners block the crawler?, Anthropic, actualizada el 07-04-2026.
  11. 11Perplexity Crawlers, Perplexity, consultada el 07-10-2026.
  12. 12Module ngx_http_map_module, nginx.org, consultada el 07-10-2026.
  13. 13CCBot, Common Crawl, consultada el 07-10-2026.
  14. 14Google's common crawlers: Google-Extended, Google Search Central, actualizada el 14-07-2026.

Cómo citar este artículo

SmoothSeen. (2026, 7 de octubre). Nginx gzip y Brotli: compresión, caché del navegador y bloquear bots de IA. https://smoothseen.com/es/blog/nginx-gzip-brotli-cache-bots/

Quién lo escribe

SmoothSeen es una herramienta de auditoría web que mide el posicionamiento en buscadores y en asistentes de IA y entrega informes con la marca de la agencia.

Este blog es de SmoothSeen: cuando un artículo habla del producto, lo hace sabiendo que es el nuestro. Las cifras de terceros enlazan a su fuente original.

Historial de cambios

  • Primera versión.

Sigue leyendo

Nginx gzip, Brotli, caché y bloquear bots de IA