> ## 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 Browser Run a fondo: Chrome sin servidor para capturas, PDF y scraping
- URL: https://blog.devutilites.com/cloudflare-browser-run-a-fondo/
- Published: 2026-10-03T03:14:00.000Z
- Updated: 2026-10-03T03:14:00.000Z
- Description: Qué es Browser Run (antes Browser Rendering): Chrome sin cabeza en la red de Cloudflare, con Quick Actions por API o control total con Puppeteer desde un Worker. Límites, precios por horas de navegador y un servicio de capturas con caché en KV probado en local.
- Author: Luis Collantes
- Tags: Cloudflare, Backend

Hay tareas que solo se pueden hacer con un navegador de verdad: sacar una captura de una página para la vista previa de un enlace, generar un PDF de una factura a partir de HTML, leer el contenido de una web que se pinta con JavaScript o comprobar que tu aplicación carga bien. Montar Chrome en un servidor propio funciona, pero consume memoria, hay que actualizarlo y escala mal.

**Browser Run** (hasta hace poco **Browser Rendering**) te da Chrome sin cabeza (*headless*) en la red de Cloudflare, sin servidor. Se puede usar de dos maneras: con **Quick Actions**, endpoints listos para capturas, PDF, Markdown o scraping, y con **sesiones de navegador**, en las que controlas Chrome desde un Worker con **Puppeteer**, **Playwright** o el protocolo CDP.

En este artículo vemos cómo funciona, sus límites y precios, y un Worker que hace capturas y extrae metadatos de páginas, con caché en KV, ejecutado en local con el Chrome que descarga Wrangler.

🧪

**El Worker de este artículo se ha ejecutado de verdad** el 3 de octubre de 2026 con **Wrangler 4.147.0**, **@cloudflare/puppeteer 1.4.0** y **Node.js 24.21.0**. En local, Wrangler descargó y usó **Chrome 126.0.6478.182**. **Las Quick Actions y la ejecución en la red de Cloudflare no las he probado**: necesitan cuenta. Límites y precios comprobados en la documentación oficial ese mismo día.

## Cómo funciona

![Esquema de Browser Run: una aplicación llama a las Quick Actions por API REST o un Worker usa el binding del navegador con Puppeteer; Cloudflare arranca un Chrome sin cabeza en su red que carga la página de destino y devuelve captura, PDF, HTML o datos](https://blog.devutilites.com/content/images/2026/10/cf-browser-run-esquema.png)

Quick Actions para lo habitual; sesiones con Puppeteer o Playwright cuando necesitas controlar cada paso. Diagrama: devutilites.

### Quick Actions

Son endpoints de la API de Cloudflare (`/accounts/<id>/browser-run/<acción>`) que reciben una URL o HTML y devuelven el resultado. No escribes código de navegador:

| Endpoint           | Devuelve                              |
| ------------------ | ------------------------------------- |
| /screenshot        | Captura de pantalla                   |
| /pdf               | PDF de la página                      |
| /content           | HTML ya renderizado                   |
| /markdown          | La página convertida a Markdown       |
| /snapshot          | Varios formatos a la vez              |
| /scrape            | Elementos concretos según selectores  |
| /links             | Los enlaces de la página              |
| /json              | Datos estructurados extraídos con IA  |
| /accessibilityTree | El árbol de accesibilidad             |
| /crawl             | Rastreo de varias páginas de un sitio |

Se pueden llamar con un token de API desde cualquier sitio, o desde un Worker mediante el binding sin token.

### Sesiones de navegador

Si necesitas hacer clic, rellenar formularios, esperar a un elemento o encadenar páginas, abres una sesión y controlas Chrome con **Puppeteer** (la versión adaptada `@cloudflare/puppeteer`), **Playwright** o **CDP** directamente. Una sesión se cierra tras **60 segundos de inactividad** por defecto; con la opción `keep_alive` puedes alargarlo hasta **10 minutos**, y puedes reconectarte a una sesión abierta para reutilizarla en otra petición.

## Límites y precios

Browser Run está disponible en **Free y Paid**. Se paga por **horas de navegador** (las de Quick Actions y las de sesiones se suman) y, solo en sesiones, por **navegadores concurrentes**:

| Concepto                            | Workers Free                   | Workers Paid                                            |
| ----------------------------------- | ------------------------------ | ------------------------------------------------------- |
| Horas de navegador                  | 10 minutos al día              | 10 horas al mes, después 0,09 $/hora                    |
| Navegadores concurrentes (sesiones) | 3                              | 10 incluidos (media mensual), después 2 $ por navegador |
| Máximo de navegadores concurrentes  | 3 por cuenta                   | 200 por cuenta                                          |
| Navegadores nuevos                  | 1 cada 20 segundos             | 3 por segundo                                           |
| Peticiones a Quick Actions          | 1 cada 10 segundos             | 30 por segundo                                          |
| Rastreos (/crawl)                   | 5 al día, 100 páginas cada uno | Sin estos límites adicionales                           |
| Inactividad antes de cerrar         | 60 s (ampliable a 10 min)      | 60 s (ampliable a 10 min)                               |

Los navegadores concurrentes se calculan como **media mensual**. El ejemplo oficial: 10 navegadores durante 15 días y 20 durante otros 15 dan una media de 15; menos los 10 incluidos, son 5 × 2 $ = 10 $ al mes.

⚠️

**Los 10 minutos diarios del plan Free se van rápido.** Una sesión que no cierras sigue contando hasta que caduca por inactividad. Cuando se agota, verás `Browser time limit exceeded for today` (código 429). Cierra siempre el navegador y cachea los resultados.

## El código

La configuración necesita el binding `browser` y, en este ejemplo, un namespace de KV para guardar las capturas:

```json
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "browser-lab",
  "main": "src/index.ts",
  "compatibility_date": "2026-10-01",
  "browser": { "binding": "NAVEGADOR" },
  "kv_namespaces": [{ "binding": "CAPTURAS", "id": "66666666666666666666666666666666" }]
}
```

El Worker tiene dos rutas: `/captura` devuelve un PNG de la página (y lo guarda un día en KV) y `/meta` abre la página y lee del DOM el título, la descripción, los `h1` y el número de enlaces:

```typescript
import puppeteer from "@cloudflare/puppeteer";

// `document` solo existe dentro del navegador (en page.evaluate), no en el Worker
declare const document: any;

// Solo dejamos capturar páginas públicas por HTTPS (evita usar el Worker como proxy)
function urlPermitida(texto: string | null): URL | null {
  try {
    const u = new URL(texto ?? "");
    return u.protocol === "https:" ? u : null;
  } catch {
    return null;
  }
}

export default {
  async fetch(request, env): Promise<Response> {
    const { pathname, searchParams } = new URL(request.url);
    const destino = urlPermitida(searchParams.get("url"));
    if (!destino) return new Response("Parámetro url no válido (solo https)", { status: 400 });

    if (pathname === "/captura") {
      const clave = `png:${destino.href}`;
      const guardada = await env.CAPTURAS.get(clave, "arrayBuffer");
      if (guardada) {
        return new Response(guardada, { headers: { "content-type": "image/png", "x-cache": "HIT" } });
      }

      const browser = await puppeteer.launch(env.NAVEGADOR);
      try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1200, height: 630 });
        await page.goto(destino.href, { waitUntil: "networkidle0", timeout: 20_000 });
        const png = await page.screenshot({ type: "png" });
        // Una captura por URL y día: ahorra horas de navegador
        await env.CAPTURAS.put(clave, png, { expirationTtl: 86_400 });
        return new Response(png, { headers: { "content-type": "image/png", "x-cache": "MISS" } });
      } finally {
        // Sin close() la sesión sigue abierta (y facturando) hasta que caduca
        await browser.close();
      }
    }

    if (pathname === "/meta") {
      const browser = await puppeteer.launch(env.NAVEGADOR);
      try {
        const page = await browser.newPage();
        await page.goto(destino.href, { waitUntil: "domcontentloaded" });
        // page.evaluate se ejecuta DENTRO del navegador, sobre el DOM ya renderizado
        const datos = await page.evaluate(() => ({
          titulo: document.title,
          descripcion: document.querySelector('meta[name="description"]')?.getAttribute("content") ?? null,
          h1: [...document.querySelectorAll("h1")].map((h: any) => h.textContent?.trim()),
          enlaces: document.querySelectorAll("a[href]").length,
        }));
        return Response.json(datos);
      } finally {
        await browser.close();
      }
    }

    return new Response("Rutas: /captura?url=… y /meta?url=…", { status: 404 });
  },
} satisfies ExportedHandler<Env>;
```

Tres decisiones de diseño: solo se aceptan URL `https:`, para que nadie use tu Worker para leer `file://` o servicios internos; el `browser.close()` va en un `finally`, para que la sesión se cierre aunque algo falle; y cada captura se cachea, porque las horas de navegador son lo que pagas.

El `declare const document: any` es un truco de TypeScript: la función que pasas a `page.evaluate` se ejecuta **dentro de Chrome**, donde sí existe `document`, pero el compilador del Worker no lo sabe.

### Ejecución en local

Con `wrangler dev`, la primera vez que se usa el binding Wrangler descarga un Chrome y lo arranca en tu máquina:

```text
├ Downloading browser...
│ downloaded browser
```

Y estas son las peticiones de prueba, contra este blog y contra `example.com`:

```text
$ curl -s "localhost:8809/meta?url=https://blog.devutilites.com/"
{"titulo":"Blog","descripcion":"Notas de programación, herramientas y tecnología, ordenadas por temas.","h1":["Notas de programación, herramientas y tecnología, ordenadas por temas."],"enlaces":49} (HTTP 200, 2.385134s)
$ curl -s "localhost:8809/meta?url=https://example.com/"
{"titulo":"Example Domain","descripcion":null,"h1":[],"enlaces":1} (HTTP 200, 0.508761s)
$ curl -s -o captura.png -D - "localhost:8809/captura?url=https://example.com/" | grep -iE "^HTTP|x-cache|content-type"
HTTP/1.1 200 OK
Content-Type: image/png
x-cache: MISS
$ curl -s -o captura.png -D - "localhost:8809/captura?url=https://example.com/" | grep -iE "^HTTP|x-cache"   # segunda vez
HTTP/1.1 200 OK
x-cache: HIT
$ curl -s "localhost:8809/captura?url=file:///etc/passwd"
Parámetro url no válido (solo https) (HTTP 400)
```

La primera captura tarda 1,2 s (arrancar Chrome, cargar la página, renderizar) y la segunda 5 ms, servida desde KV. La URL `file://` se rechaza antes de abrir el navegador. Si quieres ver lo que hace Chrome mientras desarrollas, la documentación describe un modo experimental con ventana: `X_BROWSER_HEADFUL=true npx wrangler dev`.

## Errores comunes (y cómo evitarlos)

- **No cerrar el navegador.** Es la causa número uno de agotar el cupo. `browser.close()` en un `finally`.
- **Abrir un navegador por petición sin caché.** Cachea capturas y PDF en KV o R2; reutiliza sesiones si haces muchas operaciones seguidas.
- **Aceptar cualquier URL.** Un servicio de capturas abierto es un proxy para cualquiera. Valida el esquema, limita dominios si puedes y protégelo con autenticación o [rate limiting](https://blog.devutilites.com/cloudflare-waf-ddos-a-fondo/).
- **Esperar a `networkidle0` en páginas que nunca paran.** Algunas webs hacen peticiones constantes. Pon un `timeout` o espera a un selector concreto.
- **Usar Puppeteer cuando bastaba una Quick Action.** Para una captura o un PDF, el endpoint es más simple.
- **Olvidar el límite de navegadores nuevos.** En Free solo puedes abrir uno cada 20 segundos; con varias peticiones a la vez, reutiliza la sesión.
- **Confiar en `document` fuera de `evaluate`.** En el Worker no existe DOM.

## Cuándo usar Browser Run (y cuándo no)

**Úsalo** para generar imágenes de vista previa (Open Graph), PDF de facturas o informes a partir de HTML, leer webs que se renderizan con JavaScript, convertir páginas a Markdown para alimentar un modelo de IA y pruebas sencillas de que una página carga.

**No lo necesitas** si la página que quieres leer es HTML estático: un `fetch` y [HTMLRewriter](https://blog.devutilites.com/cloudflare-workers-a-fondo/) son mucho más baratos. Y para suites grandes de tests de extremo a extremo, un Playwright en tu CI sigue siendo lo normal.

## Sigue leyendo

- [Cloudflare a fondo](https://blog.devutilites.com/cloudflare-a-fondo/): la visión general de la plataforma.
- [Cloudflare Workers a fondo](https://blog.devutilites.com/cloudflare-workers-a-fondo/): el runtime desde el que controlas el navegador.
- [Cloudflare Workers KV a fondo](https://blog.devutilites.com/cloudflare-workers-kv-a-fondo/): la caché de las capturas.
- [Cloudflare R2 a fondo](https://blog.devutilites.com/cloudflare-r2-a-fondo/): para guardar PDF y capturas grandes.
- [Cloudflare Workers AI a fondo](https://blog.devutilites.com/cloudflare-workers-ai-a-fondo/): qué hacer con el Markdown que extraes.