Ejecutar requests en paralelo
Lo que aprenderás
Cómo ejecutar un lote grande de requests de FourA de forma concurrente sin acumular errores 429, limitando la cantidad de llamadas que mantienes abiertas y reaccionando a un rechazo en lugar de repetirlo.
Requisitos previos
- Una API key de FourA (obtén una aquí)
- Python 3.9+ con
requests, o Node.js 18+
Los límites con los que trabajas
Tu plan incluye dos límites máximos por endpoint: cuántos requests pueden ejecutarse al mismo tiempo y cuántos pueden iniciarse por minuto (Browser tiene un límite por día en lugar de uno por minuto). Single, Proxy y Browser tienen cada uno sus propios números, y la pestaña Limits & Features de Usage & Limits los enumera.
El request que supera el límite de concurrencia se devuelve como HTTP 429 con X-FourA-Limit: plan_limit_concurrency y el límite en el body:
{
"error": "Concurrency limit reached: your plan allows 50 simultaneous single request(s). Retry when an in-flight request finishes.",
"reason": "plan_limit_concurrency",
"documentation": "https://foura.ai/prices",
"limit": 50,
"in_flight": 51,
"retry_after_seconds": 1
}
La que supera el límite por minuto devuelve X-FourA-Limit: plan_limit_rate y retry_after_seconds corriendo hasta el final del minuto.
Ambos rechazos son inmediatos. FourA no pone en cola la llamada para entregarla más tarde, por lo que no se gasta nada ni se factura nada. Sin embargo, las llamadas rechazadas sí cuentan para el minuto deslizante, por lo que una tormenta de reintentos prolonga su propio período de enfriamiento. Referencia completa de campos: Rate Limits.
Dos aspectos importantes que a menudo se pasan por alto:
- Una llamada
POST /api/auto/no se cuenta por sí misma, pero sí cada subllamada Single, Proxy y Browser que realiza por ti. Una llamada auto puede ocupar más de un slot mientras se ejecuta su secuencia. - Una request de Browser ocupa su slot durante todo el tiempo que tarda la página en renderizarse, lo cual es mucho más largo que una request de Single. Un lote de llamadas browser alcanza su límite con menos requests que un lote de llamadas single.
Paso 1: Limita tu propia concurrencia
Elige un número por debajo del límite para el endpoint al que estás llamando y mantenlo. Un worker pool hace esto en una sola línea:
import os
import concurrent.futures
import requests
API = "https://eu.api.foura.ai/api/single/"
KEY = os.environ["FOURA_API_KEY"]
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}
# Below your plan's Single concurrency, so a slow request never pushes the batch over it.
MAX_IN_FLIGHT = 30
def fetch_one(url):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
return url, resp.status_code, resp.json()
def fetch_all(urls):
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_IN_FLIGHT) as pool:
for outcome in concurrent.futures.as_completed(pool.submit(fetch_one, u) for u in urls):
results.append(outcome.result())
return results
max_workers es todo el mecanismo. El pool nunca tiene más llamadas abiertas que esa cantidad, por lo que el lote puede tener un millón de URLs y aun así mantenerse por debajo del límite.
Deja margen. Si dos procesos de tu lado comparten una API key, comparten el mismo límite, así que asígnale la mitad a cada uno. Si un objetivo rápido permite que el pool complete las requests más rápido de lo que permite un minuto, ajusta el ritmo de los workers para mantenerte también por debajo del límite por minuto.
Paso 2: Aplica backoff en lugar de reenviar
Si llega un rechazo, la respuesta incorrecta es reenviar el lote de inmediato. Cada llamada en él será rechazada de nuevo, y los reintentos se acumularán sobre las requests que ya estaban en ejecución.
Espera primero. El tiempo de espera está en el header Retry-After y en retry_after_seconds en el body:
import time
# Plan limits that no short wait will clear.
STOP_ON = {"plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", "plan_limit_feature", "plan_limit_premium"}
def fetch_one(url, attempts=4):
for attempt in range(attempts):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
limit = resp.headers.get("X-FourA-Limit")
if limit in STOP_ON:
raise RuntimeError(f"stopped by {limit}")
if resp.status_code not in (429, 503):
return url, resp.status_code, resp.json()
header = resp.headers.get("Retry-After")
if header and header.isdigit():
wait = int(header)
else:
body = resp.json()
wait = body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
time.sleep(wait)
return url, 429, {"error": "still refused after retries"}
Añade jitter cuando ejecutes muchos workers. Sin él, todos los workers rechazados en el mismo segundo se reactivan en el mismo segundo y vuelven a ser rechazados juntos.
import random
time.sleep(wait + random.uniform(0, 0.5))
Paso 3: Lo mismo en Node
const API = 'https://eu.api.foura.ai/api/single/';
const HEADERS = {
'X-API-Key': process.env.FOURA_API_KEY,
'Content-Type': 'application/json',
};
const MAX_IN_FLIGHT = 30;
async function fetchOne(url) {
const resp = await fetch(API, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ method: 'GET', url }),
});
return { url, status: resp.status, body: await resp.json() };
}
async function fetchAll(urls) {
const queue = [...urls];
const results = [];
async function worker() {
while (queue.length) {
results.push(await fetchOne(queue.pop()));
}
}
await Promise.all(
Array.from({ length: Math.min(MAX_IN_FLIGHT, urls.length) }, worker)
);
return results;
}
Un número fijo de workers extrayendo tareas de una sola cola mantiene abiertas exactamente esa cantidad de llamadas, sin importar el tamaño del lote.
Paso 4: Monitorea lo que realmente estás usando
Abre Usage & Limits en el dashboard. Los contadores en tiempo real junto a tus límites muestran la concurrencia, la tasa por minuto y las browser requests por día a medida que cambian, permitiéndote dimensionar MAX_IN_FLIGHT frente a una ejecución real en lugar de una suposición.
El Activity Log también registra los rechazos. Una ejecución que finalizó con la cantidad correcta de filas pero con una dispersión de resultados rate_limit se estuvo aplicando rate limit a sí misma.
Errores comunes
- Lanzar toda la lista a la vez.
asyncio.gathersobre 5,000 URLs, oPromise.allsobre un array sin límite, abre 5,000 llamadas. Limita el pool, no la lista. - Reintentar en paralelo. Reenviar cada llamada rechazada en el mismo instante en que se rechaza reproduce el pico de tráfico que causó el rechazo. Espera el
Retry-Aftery añade jitter. - Tratar un límite diario o de período como una simple espera.
plan_limit_browser_dailyse reinicia a medianoche UTC,plan_limit_creditsyplan_limit_bandwidthal final del período de facturación. Detén la ejecución y leeresets_aten el body cuando esté disponible. - Contar las llamadas auto como un solo slot cada una. La secuencia interna en
/api/auto/realiza sub-llamadas reales, y esas son las que cuentan para los límites. - Compartir una clave entre procesos sin dividir el presupuesto. Los límites máximos son por cuenta, no por proceso.
- Dimensionar un solo pool para todos los endpoints. Single, Proxy y Browser tienen cada uno su propio número de concurrencia. Un pool dimensionado para Single superará el límite máximo más reducido de Browser.
Relacionado
- Rate Limits: Cada formato de rechazo y qué significa cada campo
- Response Headers:
X-FourA-LimityRetry-After - Request Outcomes: Por qué un resultado
rate_limitnunca se factura - Usage & Limits: Dónde están los contadores en tiempo real y los números de tu plan
- Smart Fetch (Auto): Cómo una sola llamada auto se convierte en varias sub-llamadas