|

URLs presignadas de S3 y el caché de imágenes en Next.js

En pocas palabras: Las URLs presignadas de S3 cambian X-Amz-Date y X-Amz-Signature en cada carga, y el optimizador de Next.js usa la URL completa como clave de caché: cada vista era un fallo de caché. Resultado: 2,3 millones de imágenes fuente en 24 horas, contra 44.000 diarias con una ruta estable.

Según un relato publicado en dev.to el 10/10/2026, las URLs presignadas de S3 anularon la optimización de imágenes Next.js en Vercel: 2,3 millones de imágenes fuente en 24 horas, contra 44.000 diarias tras el arreglo.

La optimización de imágenes de Vercel es un servicio que transforma imágenes al vuelo (tamaño, calidad y formatos como WebP y AVIF) y guarda el resultado en su CDN. En Next.js entra en juego cuando usás el componente next/image, que reescribe el src a /_next/image?url=...&w=...&q=75. Según la documentación de Vercel, está disponible en todos los planes.

En 30 segundos

  • El relato habla de 2,3 millones de imágenes fuente en 24 horas, contra un límite de plan de 50.000 al mes.
  • La causa: una URL presignada de S3 trae un X-Amz-Date y un X-Amz-Signature distintos en cada carga, y el optimizador usa la URL completa como parte de la clave de caché.
  • El arreglo fue una ruta estable (/api/media/[assetId]) que autoriza en el servidor y responde con Cache-Control: public, max-age=31536000, immutable. El autor reporta 44.000 diarias después.
  • Es un solo caso sin verificación externa, y el límite de 50.000 no coincide con la documentación de precios consultada.

¿Qué pasó con la alerta de 2,3 millones de imágenes en Vercel?

Un martes a las 09:41 UTC, según el relato, llegó al Slack del equipo de plataforma una alerta de Vercel: Image Optimization se acercaba al límite del plan, de 50.000 imágenes fuente por mes, y la cuenta ya había usado 2,3 millones en 24 horas. Nadie había tocado el pipeline de imágenes ni el plan. Lo único nuevo era una galería de productos publicada el lunes.

Primero descartaron un scraper: el referer de casi todas las requests era el dominio de la propia app y el ritmo seguía el tráfico normal del día. Después revisaron el componente. La prop sizes seguía igual, con tres breakpoints, y 40 productos por galería dan 120 requests por vista como máximo, muy lejos de 2,3 millones.

Entonces miraron el parámetro url que recibía el optimizador. Cinco cargas del mismo avatar en dos minutos mostraron cinco strings distintos: mismo bucket, misma key, mismos bytes, pero otro X-Amz-Date y otra firma en cada una. Y la teoría del purge de caché cayó rápido, porque pidiendo la URL exacta dos veces el caché respondía en menos de 20 ms.

Ahí estaba el bug.

Dos semanas antes, un hallazgo de auditoría SOC 2 había movido las imágenes a un bucket privado, con una URL presignada de 15 minutos generada en el servidor y pasada al src. La secuencia, según el relato: cambiás el bucket a privado, generás la URL firmada en el backend, la pasás al componente, el cambio cierra el hallazgo de seguridad, nadie revisa qué hace el optimizador con esa URL y dos semanas después, con una galería nueva y cuarenta fotos por vista, aparece la alerta. Esto se conecta con lo que analizamos en cuánto cuesta realmente un proyecto Next.js en Vercel.

Ojo con el alcance: es el relato de un solo autor, sin nombre de empresa que se pueda verificar. Ni los 2,3 millones ni los US$1.840 de excedente que menciona tienen respaldo independiente. Tomalo como un caso bien descripto (que no es poco), no como un dato de mercado.

¿Cómo calcula Vercel la clave de caché en la optimización de imágenes Next.js?

Para imágenes remotas, la documentación de Vercel (actualizada el 13/08/2026) arma la clave de caché con Project ID, q (calidad), w (ancho), url (la URL absoluta de origen) y el header Accept normalizado. Si cambia un solo carácter de url, query string incluida, el optimizador busca otra entrada, no la encuentra y transforma la imagen de nuevo.

El tiempo de vida es el mayor entre el max-age del origen y minimumCacheTTL, que vale 3600 segundos por defecto. Es una variable de duración, no de identidad. Si la clave nunca se repite, alargar el TTL no cambia nada.

¿Qué es una URL presignada de S3?

Una URL presignada de S3 es un enlace temporal, firmado con las credenciales de quien lo genera, que da acceso a un objeto de un bucket privado sin que el visitante tenga cuenta de AWS, como describe la guía de AWS. La firma y el vencimiento viajan en la query string.

En el caso reportado, el vencimiento era de 900 segundos (X-Amz-Expires=900) y la firma cambiaba cada vez que el servidor generaba la URL. Eso es lo que hace a una URL presignada: si fuera siempre igual, sería un link permanente. El choque con un caché que usa la URL completa como identidad no tiene vuelta.

Lo resume el autor: “A presigned URL and a cache key are different problems wearing the same string” (una URL presignada y una clave de caché son problemas distintos con la misma cadena de texto). Ya lo cubrimos antes en nuestra comparación de Vercel y AWS Amplify con números reales.

¿Cuánto cuesta un cache miss en Vercel Image Optimization?

Con el modelo de precios vigente por defecto, Vercel cobra cada cache MISS y STALE como una transformación y como una escritura en caché, según la página de límites y precios (actualizada el 11/08/2026). Las lecturas de caché se cobran solo cuando la imagen sale del caché global compartido, no en cada HIT.

MétricaIncluido en HobbyTarifa on-demandCuándo se cobra
Transformaciones5K/mesUS$0,05 a US$0,0812 por 1KCada MISS y STALE
Lecturas de caché300K/mesUS$0,40 a US$0,64 por 1MAl leer del caché global compartido
Escrituras de caché100K/mesUS$4,00 a US$6,40 por 1MCada MISS y STALE
optimización de imágenes next.js diagrama explicativo

El rango depende de la región. El modelo anterior, por imágenes fuente únicas a US$5 por cada 1.000, solo sigue vigente para equipos Enterprise creados antes del 18/02/2025 que no migraron, y ahí Pro incluye 5.000 imágenes fuente.

Acá hay una discrepancia que el relato no resuelve. Habla de un límite de 50.000 imágenes fuente por mes y de facturar por imagen fuente única, y esa cifra no coincide con ninguna de las dos páginas. Puede ser un contrato a medida o una simplificación del autor; desde afuera no hay manera de saberlo. Por eso no armo ninguna cuenta de costos: con datos que no cierran, cualquier número sería inventado.

¿Cómo servir imágenes de S3 privado con next/image sin romper el caché?

Sacá la URL presignada del src y reemplazala por una ruta estable de tu propia app. Esa ruta verifica la autorización en el servidor, lee el objeto de S3 y responde con Cache-Control: public, max-age=31536000, immutable. El optimizador pasa a ver una url que depende solo del ID del asset, así que la clave de caché deja de cambiar.

Esto es lo que desplegó el equipo del relato, resumido (probalo en tu versión de Next.js antes de copiarlo):

// app/api/media/[assetId]/route.ts
export async function GET(req, { params }) {
 const authorized = await checkAccess(req, params.assetId);
 if (!authorized) return new Response('Forbidden', { status: 403 });

 const object = await s3.getObject({
 Bucket: 'assets-private-ourapp',
 Key: `products/${params.assetId}.jpg`,
 });

 return new Response(object.Body, {
 headers: {
 'Content-Type': 'image/jpeg',
 'Cache-Control': 'public, max-age=31536000, immutable',
 },
 });
}

En el componente, el src queda como lo siguiente (ejemplo ilustrativo mío; el post no muestra el JSX completo): Te puede servir nuestra cobertura de migrar tu app de Vercel a un servidor propio.

<Image src={`/api/media/${product.imageId}`} width={400} height={400} alt={product.name} />
  • La autorización sigue en el servidor. checkAccess corre en la ruta y la firma de S3 nunca sale del backend.
  • La identidad es estable. product.imageId no cambia para una foto dada, así que la clave se mantiene hasta que el asset se reemplace.
  • immutable tiene una condición. Es seguro porque, según el autor, una subida nueva recibe un ID nuevo. Si reusás IDs, vas a servir fotos viejas.

El autor reporta una baja de 2,3 millones a 44.000 imágenes fuente diarias y un segundo caso del mismo error en las imágenes inline de una plantilla de email.

¿Qué límites y riesgos tiene esta solución?

La solución resuelve el costo, pero deja una pregunta de seguridad abierta: puede que la autorización corra solo cuando el caché no tiene la imagen. Lo que sigue es inferencia mía a partir de la documentación, sin pruebas propias.

La clave de caché que lista Vercel no incluye al usuario. Si es así, una vez cacheada la variante optimizada, otro usuario que pida la misma combinación de url, w y q podría recibirla sin que tu ruta se ejecute. Con contenido sensible, revisá esto antes de dejar public. Tampoco queda claro qué credenciales llegan a tu ruta cuando la pide el optimizador y no el navegador. Ni el relato ni la documentación consultada lo aclaran.

Si el costo por transformación te preocupa más que el mantenimiento, otra salida es servir imágenes ya optimizadas desde infraestructura propia, por ejemplo un VPS de donweb.com. Es una alternativa a evaluar, no algo que plantee el relato, y perdés el redimensionado automático de next/image.

¿Cómo verificar si tu app tiene el mismo problema?

Esta es una propuesta editorial, no un procedimiento de la fuente ni algo que hayamos probado: Cubrimos ese tema en detalle en causas comunes de fallos de deploy en Vercel.

  • Compará el parámetro url. En la pestaña Network, filtrá por _next/image, recargá dos veces y mirá si el valor de url es idéntico para la misma imagen.
  • Buscá firmas en el src. Si ves X-Amz-Signature o cualquier timestamp, ya tenés la respuesta.
  • Mirá los estados HIT/MISS/STALE. Vercel los documenta; en uso estable, una imagen repetida no debería dar MISS en cada carga.
  • Restringí los patrones. En remotePatterns podés fijar search: '' (el ejemplo de la documentación lo trae vacío) para rechazar URLs con query string. Probalo primero en staging.
  • Poné una alerta diaria. El propio relato concluye que una variación día contra día habría detectado el problema dos semanas antes.

Qué está confirmado y qué no

DatoValorEstado
Clave de caché remota (Project ID, q, w, url, Accept)DocumentadaConfirmado en docs de Vercel
Imágenes fuente en 24 horas2,3 millonesSolo en el relato
Límite del plan50.000 por mesNo coincide con la documentación
Imágenes fuente diarias tras el arreglo44.000Solo en el relato
Excedente de las 24 horasUS$1.840No verificable
Días con el bug sin detectar14Solo en el relato
Segunda instancia en plantilla de email1Solo en el relato

El mecanismo (URL cambiante, clave que la incluye) es consistente con lo que documentan Vercel y AWS. Las magnitudes dependen de un autor que no podemos contrastar.

Si querés profundizar en esto, tenemos una nota sobre la caché de Next.js en Vercel y sus costos.

Errores comunes

  • Subir minimumCacheTTL para frenar los misses. Alarga la vida de entradas que nadie vuelve a pedir. Corregí la URL, no el TTL.
  • Pasar URLs presignadas a cualquier caché. Vale para el optimizador, para un CDN y para las plantillas de email, donde el relato encontró una segunda instancia.
  • Mirar solo el total mensual. Un acumulado que nadie revisa a mitad de ciclo esconde la fuga hasta que algo la multiplica.
  • Usar immutable con IDs reutilizables. Si reemplazás el archivo y mantenés el ID, los usuarios van a seguir viendo la foto vieja.

Preguntas Frecuentes

¿Sirve subir minimumCacheTTL si mis URLs de imagen cambian en cada carga?

No. El TTL define cuánto vive una entrada de caché (el mayor entre el max-age del origen y minimumCacheTTL, 3600 segundos por defecto), pero si la url cambia en cada carga ninguna petición vuelve a apuntar a la entrada anterior. Cada vista sigue siendo un MISS.

¿Por qué me llegó una alerta de uso de Image Optimization en Vercel?

Vercel avisa por email y en el dashboard cuando te acercás a los límites de uso de tu plan. En Pro podés configurar Spend Management para recibir notificaciones o pausar proyectos al llegar a un gasto definido. Antes de sospechar de un bot, mirá en los logs de /_next/image si el parámetro url de una misma imagen cambia entre cargas.

¿Qué pasa en Hobby cuando se supera el límite de Image Optimization?

Las imágenes nuevas fallan con un error 402, se dispara el callback onError y se muestra el texto alt en lugar de la imagen. Las ya cacheadas siguen funcionando y no se cobra por exceder el límite, según la documentación de Vercel.

¿Cuándo conviene usar unoptimized en next/image?

Conviene en íconos o miniaturas de menos de 10 KB, GIF animados, SVG e imágenes que cambian seguido, según la documentación de Vercel. Optimizar archivos que ya son chicos gasta cuota sin mejorar nada.

Conclusión

Un arreglo de seguridad pasó la revisión sin que nadie mirara su efecto en el caché, y el costo apareció dos semanas después, según un único relato que no se puede verificar. Lo que sí se puede comprobar es el mecanismo: si la URL cambia en cada carga, la clave de caché de Vercel también.

Qué hacer esta semana: buscá firmas o timestamps en cualquier src que pase por next/image y en tus plantillas de email, movelos detrás de una ruta con ID estable, validá cómo se comporta la autorización con el caché antes de usar public y sumá una alerta de variación diaria al uso de Image Optimization.

Fuentes

Te puede interesar...