Un error 429 significa que una clave de API ha agotado su presupuesto por minuto; la solución es leer desde el host de entrega en caché de Webflow y enviar las escrituras por lotes.
Los errores de límite de tasa suelen aparecer después de un lanzamiento exitoso. Una página lista elementos del CMS a través de un controlador de rutas de Webflow Cloud y, cuando aumenta el tráfico, cada visitante activa una nueva llamada a la API. La API de datos de Webflow permite 60 solicitudes por minuto por clave de API en los sitios Starter y Basic, y 120 en los planes antiguos de CMS, Ecommerce y Business.
Después de eso, devuelve un 429 con un Retry-After de unos 60 segundos, y la página falla.
En esta guía, crearemos una solución alternativa con una aplicación Next.js en Webflow Cloud leyendo el contenido publicado desde la API de entrega de contenido detrás de una caché de servidor breve, y enviando las escrituras en lotes de hasta 100 elementos por solicitud.
¿Qué necesitas para solucionar los errores de límite de tasa 429 en Webflow?
Necesitas cuatro cosas:
- Aplicación de Webflow Cloud: Una aplicación Next.js 15 o superior desplegada en Webflow Cloud, según la página de "bring-your-own-app", que también indica: "Actualmente, Webflow Cloud solo admite el gestor de paquetes npm".
- Un token exclusivo para esta aplicación: Un token de sitio con
cms:read, además decms:writesi la aplicación crea elementos. Webflow aplica límites de tasa por clave de API, por lo que un token compartido con una herramienta de automatización o un script local también comparte su presupuesto. - ID de colección: Desde la configuración de la colección en el CMS.
- Un secreto de sincronización: Una cadena larga y aleatoria que protege la ruta de escritura en el paso 5.
La primera tarea es asignar un propietario a cada llamada a la API para que puedas ver a dónde va el presupuesto.
5 pasos para solucionar los errores de límite de tasa 429 en Webflow Cloud
Centraliza las llamadas a la API, lee los encabezados de límite, mueve las lecturas a la API de entrega de contenido, almacénalas en caché con una alternativa obsoleta y procesa las escrituras por lotes.
1. Coloca el token en las variables de entorno y enruta cada llamada a través de un único contenedor
Mueve el token y el ID de la colección a las variables de entorno de Webflow Cloud, luego haz que un solo módulo sea el único lugar de la aplicación que llame a la API. Un 429 es un problema de presupuesto, y un presupuesto solo se puede gestionar desde un lugar: tres componentes que consultan la API por su cuenta superarán un límite que una llamada compartida nunca alcanzaría.
En la configuración de variables de tu entorno de Webflow Cloud, añade WEBFLOW_API_TOKEN y SYNC_SECRET como secretos y WEBFLOW_COLLECTION_ID como variable simple.
La documentación de variables de entorno de Webflow confirma que ambos tipos llegan a la aplicación: "Tanto las variables de entorno secretas como las no secretas están disponibles para el proceso de compilación de tu aplicación y para la aplicación desplegada en tiempo de ejecución". Localmente, coloca los mismos tres nombres en .env.local.
Crea el módulo contenedor que importará cada ruta:
// lib/webflow.ts
const API = 'https://api.webflow.com/v2';
const CDN = 'https://api-cdn.webflow.com/v2';
export class RateLimited extends Error {
retryAfterSeconds: number;
constructor(retryAfterSeconds: number) {
super(`Webflow API rate limit, retry after ${retryAfterSeconds}s`);
this.retryAfterSeconds = retryAfterSeconds;
}
}
export async function webflowFetch(url: string, init: RequestInit = {}): Promise<Response> {
const headers = new Headers(init.headers);
headers.set('Authorization', `Bearer ${process.env.WEBFLOW_API_TOKEN}`);
headers.set('accept', 'application/json');
const res = await fetch(url, { ...init, headers });
if (res.status === 429) {
throw new RateLimited(Number(res.headers.get('retry-after')) || 60);
}
return res;
}
export function liveItemsUrl(offset = 0): string {
return `${CDN}/collections/${process.env.WEBFLOW_COLLECTION_ID}/items/live?limit=100&offset=${offset}`;
}
export function stagedItemsUrl(): string {
return `${API}/collections/${process.env.WEBFLOW_COLLECTION_ID}/items?limit=1`;
}
export function insertItemsUrl(): string {
return `${API}/collections/${process.env.WEBFLOW_COLLECTION_ID}/items/insert`;
}
Ahora, todas las rutas obtienen el token y las URL de los endpoints desde un mismo módulo. Un error 429 aparece como un RateLimited error que incluye el Retry-After de Webflow, por lo que cada llamador decide qué hacer con él en lugar de reintentar sin pensar. Buscar Authorization en el proyecto ahora devuelve exactamente un archivo.
2. Leer los encabezados de límite en una ruta de diagnóstico
Webflow informa el presupuesto en cada respuesta de la API de datos. <a href="https://developers.webflow.com/data/reference/rate-limits">X-RateLimit-Limit</a> es la asignación por minuto para la clave, X-RateLimit-Remaining es lo que queda en el minuto actual, y un 429 añade Retry-After, normalmente 60 segundos.
Una ruta temporal que realiza una llamada y devuelve esos encabezados te muestra qué tan cerca está la aplicación del límite antes de que algo falle.
Añadir app/api/diagnose/route.ts:
// app/api/diagnose/route.ts
import { RateLimited, stagedItemsUrl, webflowFetch } from '@/lib/webflow';
export async function GET() {
try {
const res = await webflowFetch(stagedItemsUrl());
return Response.json({
status: res.status,
limit: res.headers.get('x-ratelimit-limit'),
remaining: res.headers.get('x-ratelimit-remaining'),
});
} catch (error) {
if (error instanceof RateLimited) {
return Response.json({ status: 429, retryAfter: error.retryAfterSeconds });
}
throw error;
}
}
Ejecutar npm run dev, abrir /api/diagnose, y recargar un par de veces. remaining debería disminuir en uno por cada recarga. Si ya está cerca de cero en un sitio con poco tráfico, otro llamador está consumiendo el presupuesto de la misma clave, lo cual se trata en la sección de solución de problemas.
No añadas export const runtime = 'edge' a este archivo, aunque la página de "bring-your-own-app" lo sugiera para rutas de API. La directiva apunta al entorno de ejecución edge de Next.js, el cual no es compatible con el adaptador OpenNext de Cloudflare que utiliza Webflow Cloud.
La ruta no tiene autenticación y consume llamadas a la API de cualquiera que la cargue, así que elimínala una vez que tengas tus números.
3. Leer elementos publicados desde la API de entrega de contenido
La mayoría de los errores 429 en una página CMS provienen de lecturas, y las lecturas tienen un mejor host. La API de entrega de contenido sirve los mismos elementos en vivo desde api-cdn.webflow.com, almacena en caché cada respuesta hasta por cinco minutos y no cuenta las respuestas en caché contra tu límite de tasa. Es de solo lectura y sirve únicamente contenido publicado, que es exactamente lo que muestra una página pública.
El liveItemsUrl ayudante del paso 1 ya apunta a ese host, por lo que la ruta de lectura no necesita configuración adicional. Una llamada de lista devuelve como máximo 100 elementos, por lo que una colección más grande necesita una llamada por página, y el cargador del paso 4 realiza la paginación. Un encabezado de respuesta cf-cache-status: HIT te indica que una lectura provino de la caché y no tuvo costo.
Los borradores nunca aparecen en este host. Si una página de administración necesita borradores, lee /items desde api.webflow.com en su lugar, y mantén esa página protegida con autenticación, porque esas llamadas sí cuentan contra el límite.
4. Almacenar lecturas en caché en memoria y servir contenido obsoleto durante un 429
Envuelve la ruta de lectura en una pequeña caché de tiempo de vida (TTL) y, cuando una carga falle, devuelve el valor anterior en lugar de un error. La API de entrega de contenido reduce la frecuencia con la que las lecturas consumen presupuesto; esta caché reduce la frecuencia con la que la aplicación realiza llamadas y mantiene la página activa si una llamada upstream falla.
La caché es un Map en la memoria del módulo. Cloudflare señala que los aislamientos de Worker no son necesariamente de larga duración, por lo que cada instancia mantiene su propia copia y una instancia fría comienza vacía. Aun así, prefiero mostrar una lista de hace un minuto que una página de error
Crea el módulo de caché:
// lib/cache.ts
type Entry<T> = { value: T; expires: number };
const store = new Map<string, Entry<unknown>>();
export async function cached<T>(key: string, ttlMs: number, load: () => Promise<T>): Promise<T> {
const hit = store.get(key) as Entry<T> | undefined;
if (hit && hit.expires > Date.now()) return hit.value;
try {
const value = await load();
store.set(key, { value, expires: Date.now() + ttlMs });
return value;
} catch (err) {
if (hit) {
store.set(key, { value: hit.value, expires: Date.now() + 15_000 });
return hit.value;
}
throw err;
}
}
Cuando una carga falla, la caché almacena el último valor válido durante 15 segundos, por lo que la ruta lo vuelve a servir en lugar de llamar a la API en cada solicitud.
Almacenamiento en caché de rutas de lectura
Después de eso, escribe la ruta de lectura para paginar a través de la colección dentro de la caché:
// app/api/items/route.ts
import { cached } from '@/lib/cache';
import { liveItemsUrl, webflowFetch } from '@/lib/webflow';
type Item = { id: string; fieldData: Record<string, unknown> };
type Page = { items: Item[]; pagination: { offset: number; limit: number; total: number } };
async function loadAll(): Promise<Item[]> {
const items: Item[] = [];
let offset = 0;
while (true) {
const res = await webflowFetch(liveItemsUrl(offset));
if (!res.ok) throw new Error(`Webflow API ${res.status}`);
const page = (await res.json()) as Page;
items.push(...page.items.map(({ id, fieldData }) => ({ id, fieldData })));
offset += page.pagination.limit;
if (page.items.length === 0 || offset >= page.pagination.total) return items;
}
}
export async function GET() {
try {
const items = await cached('items', 60_000, loadAll);
return Response.json({ items });
} catch {
return Response.json({ error: 'upstream_unavailable' }, { status: 503 });
}
}
Webflow Cloud reemplaza cualquier Cache-Control encabezado que una ruta establezca con private, no-cache, por lo que el almacenamiento en caché del navegador no está disponible aquí, y solo controlas la caché del servidor.
El código del navegador que llama a la ruta debe incluir la ruta de montaje, ya que Webflow Cloud inyecta la ruta base durante la compilación y no reescribe las llamadas fetch del lado del cliente.
Gestión de la ruta base para llamadas del cliente
Añade NEXT_PUBLIC_BASE_PATH al entorno con la ruta de montaje como valor; Webflow Cloud no la crea por ti.
Un componente de cliente construye su URL de solicitud a partir de esa variable:
// app/components/items-list.tsx
'use client';
import { useEffect, useState } from 'react';
type Item = { id: string; fieldData: { name?: string } };
export default function ItemsList() {
const [items, setItems] = useState<Item[] | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const base = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
fetch(`${base}/api/items`)
.then((r) => {
if (!r.ok) throw new Error(String(r.status));
return r.json();
})
.then((data: { items: Item[] }) => setItems(data.items))
.catch(() => setFailed(true));
}, []);
if (failed) return <p>Items are unavailable right now.</p>;
if (!items) return <p>Loading...</p>;
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.fieldData.name ?? item.id}</li>
))}
</ul>
);
}
Recarga la página varias veces en un minuto y observa /api/items en el panel de Red. Las cargas repetidas que llegan a una instancia activa se sirven desde la memoria y no realizan llamadas al origen dentro del TTL de 60 segundos; el error 503 solo aparece cuando una instancia no tiene nada en caché y la llamada al origen falla.
5. Agrupa las escrituras masivas y protege la ruta de sincronización con un secreto compartido
Las escrituras no pueden usar el host de entrega, así que ahí es donde se consume el presupuesto restante. La solución es hacer menos solicitudes, no más lentas.
El endpoint Create Items de Webflow, POST /collections/{collection_id}/items/insert, acepta hasta 100 elementos en una sola solicitud, y cada solicitud cuesta una unidad del presupuesto por minuto. Enviar cincuenta elementos uno por uno cuesta cincuenta unidades; enviarlos juntos cuesta solo una.
Añade la ruta de sincronización:
// app/api/sync/route.ts
import { insertItemsUrl, RateLimited, webflowFetch } from '@/lib/webflow';
const BATCH = 100;
const MAX_ITEMS = 500;
type NewItem = { fieldData: { name: string; slug: string; [field: string]: unknown } };
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
function isNewItem(value: unknown): value is NewItem {
const fieldData = (value as NewItem)?.fieldData;
return typeof fieldData?.name === 'string' && typeof fieldData?.slug === 'string';
}
export async function POST(req: Request) {
const secret = process.env.SYNC_SECRET;
if (!secret) return new Response('Server misconfigured', { status: 500 });
if (!safeEqual(req.headers.get('x-sync-secret') ?? '', secret)) {
return new Response('Unauthorized', { status: 401 });
}
const payload = (await req.json().catch(() => null)) as { items?: unknown[] } | null;
const items = payload?.items;
if (!Array.isArray(items) || items.length === 0 || items.length > MAX_ITEMS || !items.every(isNewItem)) {
return Response.json(
{ error: `Send 1 to ${MAX_ITEMS} items, each with fieldData.name and fieldData.slug` },
{ status: 400 },
);
}
let created = 0;
for (let start = 0; start < items.length; start += BATCH) {
const batch = items.slice(start, start + BATCH);
try {
const res = await webflowFetch(insertItemsUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: batch }),
});
if (!res.ok) {
return Response.json({ created, failedAt: start, status: res.status, detail: await res.text() }, { status: 502 });
}
created += batch.length;
} catch (error) {
if (error instanceof RateLimited) {
return Response.json(
{ created, failedAt: start, retryAfter: error.retryAfterSeconds },
{ status: 429, headers: { 'Retry-After': String(error.retryAfterSeconds) } },
);
}
throw error;
}
}
return Response.json({ created });
}
La ruta limita la llamada a 500 elementos, es decir, cinco solicitudes de inserción, lo cual termina muy por debajo del tiempo de espera de solicitud de 20 segundos de Webflow Cloud. No espera a que pase un error 429 dentro de la solicitud, porque el Retry-After de Webflow de unos 60 segundos es más largo de lo que puede durar la solicitud.
En su lugar, se detiene, informa cuántos elementos se crearon y dónde empezó el fallo, y pasa Retry-After para que el emisor pueda reanudar desde failedAt después de la espera. Los elementos se crean como borradores a menos que un elemento establezca isDraft: false.
La función safeEqual escrita a mano sale antes de tiempo solo si hay una discrepancia en la longitud, lo que revela la longitud del secreto pero no su contenido, y funciona tanto localmente como en Workers, a diferencia de crypto.subtle.timingSafeEqual.
Prueba de escrituras por lotes
Envía una pequeña carga de prueba desde una terminal, donde items.json contiene un array de items y cada entrada tiene un objeto fieldData con al menos name y slug:
curl -X POST http://localhost:3000/api/sync \
-H "x-sync-secret: $SYNC_SECRET" \
-H "Content-Type: application/json" \
-d @items.json
Una respuesta de {"created": N} que coincida con el número de elementos enviados significa que el lote se procesó en una sola solicitud por cada 100 elementos.
¿Qué hace que una solución para el error 429 falle en Webflow Cloud?
Revisa primero la compilación, luego el presupuesto compartido y después la caché. Cada entrada parte de lo que ves en el registro de despliegue o en el panel de Red.
La compilación de Webflow Cloud falla con un error de tiempo de ejecución (runtime) de borde no compatible
Causa: Webflow Cloud se ejecuta en Cloudflare Workers, una plataforma de borde, y la página de "bring-your-own-app" lo describe como un despliegue "usando el Edge runtime". Esa misma página indica añadir export const runtime = 'edge' a las rutas de API. Esa directiva es el objetivo del Edge runtime de Next.js, algo distinto, y el adaptador OpenNext Cloudflare no lo admite.
Solución: Elimina la directiva de cada Route Handler y deja el runtime sin configurar; las rutas se ejecutarán en Workers sin ella. El middleware sigue su propia regla: middleware.ts funciona en Workers, mientras que proxy.ts de Next.js 16 se ejecuta en Node y no lo hace.
La documentación sobre personalización del framework indica que "el middleware del entorno de ejecución de Node.js no es compatible. Solo el middleware del entorno de ejecución Edge funciona en el entorno de ejecución de Workers".
La lista de elementos devuelve un error 404 en el sitio desplegado, pero funciona localmente
Causa: Webflow Cloud monta la aplicación en una ruta e inyecta esa ruta base durante la compilación, pero las llamadas fetch del lado del cliente no se reescriben. La página de "bring-your-own-app" lo dice claramente: "Las llamadas fetch del lado del cliente deben incluir manualmente la ruta base para llegar correctamente a tus endpoints". Localmente, no hay ruta de montaje, por lo que fetch("/api/items") funciona; al desplegar, la misma llamada termina fuera de la aplicación.
Solución: Configura NEXT_PUBLIC_BASE_PATH en la ruta de montaje dentro del entorno de Webflow Cloud, antepón a cada URL del lado del cliente dicha ruta tal como hace items-list.tsx y vuelve a desplegar para que el valor se incluya en el bundle del cliente.
Siguen apareciendo errores 429 en /api/diagnose aunque el sitio no tenga visitantes
Causa: La aplicación no es la única que realiza llamadas. Webflow aplica el límite por clave de API, por lo que cualquier automatización o script en la máquina de alguien que utilice el mismo token puede consumir el presupuesto de la aplicación.
Solución: Asigna a la aplicación su propio token para que ninguna otra integración pueda consumir su presupuesto. Luego, identifica a los otros emisores revisando tus herramientas de automatización y cualquier script que llame a la API de Webflow para este sitio, y traslada sus tareas masivas a horas de poco tráfico. Recarga /api/diagnose y confirma que remaining se mantiene cerca del límite.
Los errores 429 regresan en ráfagas cortas después de un despliegue
Causa: La caché en memoria se mantiene por instancia de Worker, por lo que varias instancias frías que se inician en el mismo minuto realizan su propia primera llamada. Las lecturas a través de la API de entrega de contenido (Content Delivery API) absorben esto en su mayoría, por lo que las ráfagas que persisten suelen provenir de una ruta que sigue leyendo api.webflow.com.
Solución: Verifica que toda lectura pública pase por liveItemsUrl. Si las ráfagas persisten, aumenta el TTL pasado a cached(), por ejemplo de 60_000 a 300_000, lo cual coincide con la ventana de caché del propio host de entrega. Para una caché compartida por todas las instancias, muévela a un binding de Key Value Store con un TTL mínimo de 60 segundos.
Qué puedes construir a continuación con la API de Webflow y Webflow Cloud
Un índice de búsqueda reconstruido mediante programación es lo que yo construiría a continuación con el mismo wrapper y caché, ya que lee toda la colección a través del host de entrega y no escribe nada de vuelta. Para una sincronización bidireccional con una base de datos propia, la guía de base de datos externa utiliza el mismo bucle de paginación.
Para una personalización más profunda más allá de lo que cubren este wrapper y esta caché, la documentación para desarrolladores de Webflow incluye la referencia completa de la Data API.
Preguntas frecuentes
¿Puede una sincronización reanudada crear elementos CMS duplicados?
Puede ocurrir si reenvías elementos que ya fueron procesados. La ruta se detiene en el primer lote fallido y devuelve created y failedAt, así que reanuda enviando solo los elementos desde failedAt en adelante. Un error 429 significa que Webflow rechazó ese lote por completo, por lo que no se creó nada en él.
¿Debería la aplicación esperar y reintentar cuando recibe un 429?
No dentro de la solicitud de un visitante. El Retry-After de Webflow suele ser de 60 segundos, y Webflow Cloud finaliza una solicitud después de 20, por lo que esperar provocaría un tiempo de espera agotado de todos modos. Sirve el valor en caché a los visitantes y deja que un trabajo de sincronización espere el Retry-After entre llamadas.
¿Puedo persistir la caché en un archivo JSON con node:fs en lugar de un Map?
No en Webflow Cloud. Los Workers solo proporcionan node:fs en una fecha de compatibilidad posterior a la que utiliza la configuración de Webflow Cloud, e incluso entonces, sus archivos no persisten entre solicitudes. Para una caché que sobreviva a reinicios y sea compartida por cada instancia, utiliza un binding de Key Value Store.
¿Es la asignación de solicitudes de Webflow Cloud el mismo límite que un error 429 de la Data API?
No. Webflow Cloud contabiliza las solicitudes que recibe tu aplicación, y su documentación especifica que las solicitudes que tu aplicación realiza a otros servicios no se suman a ese total. Un error 429 proviene del límite por clave de la propia API de datos para las llamadas que tu aplicación envía a Webflow.
¿El retraso de 60 segundos de la caché impide que las ediciones del CMS se muestren en la página?
Sí. Una edición publicada puede tardar hasta 60 segundos de TTL en cached() llegar a la ruta, además de hasta cinco minutos en la propia caché de la API de entrega de contenido. Para obtener actualizaciones más rápidas, reduce el TTL; el intervalo del host de entrega es fijo.





