> ## Content Index
> Fetch the complete content index at: https://blog.devutilites.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Cloudflare Images a fondo: redimensiona, convierte y sirve imágenes desde el borde
- URL: https://blog.devutilites.com/cloudflare-images-a-fondo/
- Published: 2026-10-03T03:14:00.000Z
- Updated: 2026-10-03T03:14:00.000Z
- Description: Cómo funciona Cloudflare Images: transformaciones por URL, por fetch o con el binding de Workers, almacenamiento con variantes, precios por transformación única y límites. Con un Worker que sirve miniaturas WebP y AVIF desde R2 probado en local.
- Author: Luis Collantes
- Tags: Cloudflare, Backend

Las imágenes suelen ser lo que más pesa de una web. Subir una foto de 4000 píxeles y mostrarla a 400 es tirar ancho de banda; servir JPEG a un navegador que entiende AVIF, también. La solución clásica es generar tú mismo todas las versiones en cada subida, guardarlas y elegir cuál servir. Funciona, pero es trabajo y ocupa espacio.

**Cloudflare Images** hace las versiones al vuelo: pides la imagen con el tamaño y el formato que necesitas y Cloudflare la redimensiona, la convierte y la cachea. Sirve tanto para imágenes que ya tienes en tu servidor o en R2 como para imágenes que subes a su propio almacenamiento.

En este artículo vemos las formas de usarlo, cómo se factura (que es lo que más confunde), sus límites y un Worker que lee originales de R2 y sirve miniaturas en WebP o AVIF con el binding de Images, probado en local.

🧪

**El Worker de este artículo se ha ejecutado de verdad** el 3 de octubre de 2026 con **Wrangler 4.147.0** y **Node.js 24.21.0**, usando el binding de Images en su **versión local de baja fidelidad**, que solo soporta parte de las funciones (redimensionar, rotar, convertir). **Las transformaciones por URL, el almacenamiento de Images con variantes y las superposiciones (`.draw()`) no los he probado**: necesitan una zona o una cuenta. Precios y límites comprobados en la documentación oficial ese mismo día.

## Cómo funciona

Hay dos ideas separadas: **transformar** (redimensionar, recortar, convertir de formato, desenfocar, añadir una marca de agua…) y **almacenar**. Puedes transformar imágenes que están en cualquier sitio, o guardarlas en Images y servirlas desde allí.

![Esquema de Cloudflare Images: el original puede estar en tu servidor, en R2 o en el almacenamiento de Images; se transforma por URL /cdn-cgi/image, con fetch y opciones cf.image en un Worker, o con el binding IMAGES; el resultado se entrega en el formato y tamaño pedidos](https://blog.devutilites.com/content/images/2026/10/cf-images-esquema.png)

Tres formas de transformar, dos sitios donde puede vivir el original. Diagrama: devutilites.

### Tres formas de transformar

1. **Por URL.** Activas las transformaciones en tu zona y pides la imagen con las opciones en la ruta: `https://tudominio.com/cdn-cgi/image/width=400,format=auto/fotos/playa.jpg`. Por defecto solo acepta originales de tu propia zona; puedes añadir otros orígenes permitidos.
2. **Con `fetch` en un Worker.** Haces un `fetch` a la imagen original con `cf: { image: { width: 400, format: "avif" } }`. Te deja decidir las opciones en código (según el dispositivo, el usuario, la ruta).
3. **Con el binding `IMAGES`.** Le pasas los **bytes** de la imagen (de R2, de una subida, de un `fetch`) y encadenas operaciones: `.input()`, `.transform()`, `.draw()` y `.output()`. No necesita que el original tenga URL pública y te deja controlar el orden de las operaciones.

Las opciones disponibles son las mismas en todos los casos: `width`, `height`, `fit`, `gravity` (incluido recorte centrado en caras), `quality`, `format` (`auto` elige AVIF o WebP según el navegador), `rotate`, `blur`, `sharpen`, `trim`, `background`, `onerror` y unas cuantas más.

### Almacenamiento con variantes

Si subes las imágenes a Images (por el panel, la API, una URL de subida directa o el binding), se sirven desde `https://imagedelivery.net/<hash de tu cuenta>/<id de imagen>/<variante>`. Una **variante** es un preset con nombre (`miniatura`, `public`, `portada`) que define tamaño y ajuste. También puedes servir imágenes privadas con URL firmadas.

## Precios

Por defecto todas las cuentas están en el **plan Free de Images**, que incluye las transformaciones de imágenes que guardas fuera (tu servidor, R2), pero no el almacenamiento en Images.

| Concepto                         | Free          | Paid                                         |
| -------------------------------- | ------------- | -------------------------------------------- |
| Transformaciones únicas          | 5000 al mes   | 5000 incluidas, después 0,50 $ por cada 1000 |
| Imágenes almacenadas en Images   | No disponible | 5 $ por cada 100 000 al mes                  |
| Imágenes entregadas desde Images | No disponible | 1 $ por cada 100 000 al mes                  |

La clave está en qué es una **transformación única**: una combinación de imagen original y parámetros, **contada una sola vez por mes**. `playa.jpg` a 100×100 es una; a 200×200, otra. Pedir mil veces la misma versión en el mismo mes cuenta como una. Y `format=auto` cuenta como una aunque a unos usuarios les llegue AVIF y a otros WebP.

En el plan Free, al pasar de 5000 transformaciones únicas en el mes las que ya están en caché se siguen sirviendo, pero las **nuevas devuelven el error 9422**; no te cobran nada. Si el original está en tu misma zona, `onerror=redirect` sirve el original en ese caso.

El ejemplo oficial: 2000 imágenes en 5 tamaños son 10 000 transformaciones únicas; menos las 5000 incluidas, 5000 × 0,50 $ / 1000 = **2,50 $ al mes**, más lo que cueste guardar los originales en R2.

## Límites

| Límite                          | Imágenes remotas | Imágenes en Images |
| ------------------------------- | ---------------- | ------------------ |
| Tamaño del fichero              | 100 MB           | 10 MB              |
| Área máxima                     | 100 megapíxeles  | 100 megapíxeles    |
| Lado máximo (salvo WebP y AVIF) | 12 000 px        | 12 000 px          |
| Lado máximo en AVIF             | 1200 px          | 1200 px            |
| Metadatos                       | —                | 1024 bytes         |

Con el binding, `.input()` acepta hasta **20 MB**. Las animaciones de más de 50 megapíxeles en total (sumando fotogramas) se entregan sin transformar; para eso es mejor usar vídeo.

## El código

Un Worker que guarda los originales en R2 y sirve versiones bajo demanda. La configuración:

```json
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "images-lab",
  "main": "src/index.ts",
  "compatibility_date": "2026-10-01",
  "images": { "binding": "IMAGES" },
  "r2_buckets": [{ "binding": "ORIGINALES", "bucket_name": "fotos-originales" }]
}
```

Y el Worker. `/info/<clave>` devuelve los datos del original sin transformarlo; `/img/<clave>?w=400&f=webp`, la versión pedida:

```typescript
// Formatos de salida que permitimos y sus tipos MIME
const FORMATOS = { webp: "image/webp", avif: "image/avif", jpeg: "image/jpeg", png: "image/png" } as const;
type Formato = keyof typeof FORMATOS;

export default {
  async fetch(request, env): Promise<Response> {
    const url = new URL(request.url);
    // /img/<clave>?w=400&f=webp   o   /info/<clave>
    const [, accion, ...resto] = url.pathname.split("/");
    const clave = decodeURIComponent(resto.join("/"));

    const original = await env.ORIGINALES.get(clave);
    if (!original) return new Response("No existe", { status: 404 });

    if (accion === "info") {
      // Formato, peso y dimensiones del original, sin transformarlo
      return Response.json(await env.IMAGES.info(original.body));
    }

    if (accion === "img") {
      // Lista cerrada de anchos: cada combinación distinta es una transformación facturable
      const ancho = [200, 400, 800, 1200].includes(Number(url.searchParams.get("w")))
        ? Number(url.searchParams.get("w"))
        : 800;
      const f = (url.searchParams.get("f") ?? "webp") as Formato;
      if (!(f in FORMATOS)) return new Response("Formato no permitido", { status: 400 });

      const resultado = await env.IMAGES.input(original.body)
        .transform({ width: ancho })
        .output({ format: FORMATOS[f], quality: 80 });

      return resultado.response({
        headers: { "Cache-Control": "public, max-age=86400" },
      });
    }

    return new Response("Rutas: /img/<clave>?w=&f= y /info/<clave>", { status: 404 });
  },
} satisfies ExportedHandler<Env>;
```

La lista cerrada de anchos no es un capricho: como cada combinación distinta es una transformación facturable, dejar que el cliente pida `w=401`, `w=402`… permitiría a cualquiera gastar tu cupo.

### Ejecución en local

Subí al R2 local una de las portadas de esta serie (1600×900, JPEG) con `npx wrangler r2 object put fotos-originales/portadas/workers.jpg --file foto.jpg --local` y probé varias combinaciones:

```text
$ curl -s localhost:8810/info/portadas/workers.jpg
{"format":"image/jpeg","fileSize":113724,"width":1600,"height":900}
$ curl -s -o salida.img -w "%{http_code} %{content_type} %{size_download} bytes\n" "localhost:8810/img/portadas/workers.jpg?w=400&f=webp"
200 image/webp 6440 bytes
$ curl -s -o salida.img -w "%{http_code} %{content_type} %{size_download} bytes\n" "localhost:8810/img/portadas/workers.jpg?w=200&f=avif"
200 image/avif 1869 bytes
$ curl -s -o salida.img -w "%{http_code} %{content_type} %{size_download} bytes\n" "localhost:8810/img/portadas/workers.jpg?w=1200&f=jpeg"
200 image/jpeg 61221 bytes
$ curl -s -o salida.img -w "%{http_code} %{content_type} %{size_download} bytes\n" "localhost:8810/img/portadas/workers.jpg?w=999&f=png"
200 image/png 164819 bytes
$ curl -s -o salida.img -w "%{http_code} %{content_type} %{size_download} bytes\n" "localhost:8810/img/portadas/workers.jpg?w=400&f=gif"
400 text/plain;charset=UTF-8 20 bytes
```

De 111 KB a **6,4 KB en WebP a 400 px** y **1,9 KB en AVIF a 200 px**. Comprobé el WebP con otra herramienta: mide 400×225, así que respeta la proporción. El ancho no permitido (`999`) cae al valor por defecto (800) y el GIF se rechaza.

Dos avisos sobre lo local. Wrangler usa una versión **de baja fidelidad** que no implementa todo; para probar el servicio real está `wrangler dev --remote`, que ya gasta transformaciones. Y las respuestas del binding **no se cachean solas**: la documentación recomienda activar la caché de Workers en la configuración (`"cache": { "enabled": true }`) y poner `Cache-Control`, como hace el ejemplo con `.response({ headers })`. Esa opción de configuración no la he probado.

## Errores comunes (y cómo evitarlos)

- **Dejar los parámetros libres.** Cada combinación nueva es una transformación facturable. Fija una lista de tamaños.
- **No cachear la salida del binding.** Cada petición sin caché decodifica y vuelve a codificar la imagen.
- **Pedir AVIF enormes.** AVIF está limitado a 1200 px por lado. Para imágenes grandes, usa `format=auto` o WebP.
- **Abrir las transformaciones por URL a cualquier origen.** Cualquiera podría usar tu zona para transformar imágenes de otros. Limita los orígenes permitidos.
- **Confundir planes.** El plan Free solo transforma imágenes guardadas fuera; para guardarlas en Images necesitas el Paid.
- **Servir GIF animados grandes.** Pesan mucho y por encima de 50 megapíxeles no se transforman. Convierte a MP4 o WebM.

## Cuándo usar Images (y cuándo no)

**Úsalo** para servir imágenes responsive sin generar versiones a mano, para convertir automáticamente a AVIF o WebP, para miniaturas de contenido que suben tus usuarios (originales en R2, versiones al vuelo) y para marcas de agua o recortes centrados en caras.

**No lo necesitas** si tienes pocas imágenes fijas que ya optimizas al compilar la web, ni si tu CMS ya genera y sirve las versiones (Ghost, por ejemplo, genera sus propios tamaños). Para vídeo, el producto es [Stream](https://blog.devutilites.com/cloudflare-stream-a-fondo/).

## Sigue leyendo

- [Cloudflare a fondo](https://blog.devutilites.com/cloudflare-a-fondo/): la visión general de la plataforma.
- [Cloudflare R2 a fondo](https://blog.devutilites.com/cloudflare-r2-a-fondo/): dónde guardar los originales.
- [Cloudflare CDN y caché a fondo](https://blog.devutilites.com/cloudflare-cdn-cache-a-fondo/): cómo se cachean las versiones.
- [Cloudflare Stream a fondo](https://blog.devutilites.com/cloudflare-stream-a-fondo/): lo mismo, pero para vídeo.
- [Cloudflare Workers a fondo](https://blog.devutilites.com/cloudflare-workers-a-fondo/): el runtime del binding.