Chạy các request song song

Nội dung bạn sẽ tìm hiểu

Cách chạy đồng thời một lượng lớn request FourA mà không gặp lỗi 429, bằng cách giới hạn số lượng lệnh gọi mở cùng lúc và xử lý khi bị từ chối thay vì gửi lại liên tục.

Điều kiện tiên quyết

  • Một FourA API key (lấy tại đây)
  • Python 3.9+ cùng requests, hoặc Node.js 18+

Các giới hạn cần lưu ý

Gói dịch vụ của bạn có hai mức giới hạn cho mỗi endpoint: số lượng request có thể chạy cùng lúc và số lượng request có thể bắt đầu mỗi phút (Browser áp dụng hạn mức theo ngày thay vì theo phút). Single, Proxy và Browser đều có các thông số riêng, được liệt kê trong tab Limits & Features tại Usage & Limits.

Request vượt quá giới hạn đồng thời sẽ trả về mã HTTP 429 cùng X-FourA-Limit: plan_limit_concurrency và thông tin mức giới hạn trong 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
}

Yêu cầu vượt quá giới hạn mỗi phút sẽ trả về X-FourA-Limit: plan_limit_rateretry_after_seconds kéo dài đến hết phút đó.

Cả hai trường hợp từ chối đều diễn ra ngay lập tức. FourA không xếp hàng lệnh gọi để xử lý lại sau, do đó không có tài nguyên nào bị tiêu tốn và không bị tính phí. Tuy nhiên, các lệnh gọi bị từ chối vẫn được tính vào khoảng thời gian sliding minute, vì vậy một đợt retry storm sẽ kéo dài thời gian cooldown của chính nó. Tài liệu tham khảo đầy đủ về các trường: Rate Limits.

Hai điểm quan trọng thường bị bỏ qua:

  • Bản thân một lệnh gọi POST /api/auto/ không bị tính riêng, nhưng mọi sub-call Single, Proxy và Browser mà nó thực hiện thay bạn đều được tính. Một lệnh gọi auto có thể chiếm nhiều hơn một slot trong khi ladder của nó đang chạy.
  • Một Browser request chiếm slot của nó trong suốt thời gian trang render, lâu hơn nhiều so với một Single request. Một đợt browser call sẽ đạt đến mức trần với số lượng request ít hơn so với một đợt single call.

Bước 1: Giới hạn concurrency của chính bạn

Chọn một con số dưới mức trần cho endpoint bạn đang gọi và duy trì nó. Một worker pool có thể xử lý việc này chỉ với một dòng:

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 là toàn bộ cơ chế. Pool không bao giờ mở nhiều hơn số lượng lệnh gọi đó, vì vậy batch có thể dài tới một triệu URL mà vẫn nằm dưới giới hạn.

Hãy để lại khoảng dự phòng. Nếu hai tiến trình phía bạn dùng chung một API key, chúng sẽ dùng chung một mức trần, vì vậy hãy chia cho mỗi tiến trình một nửa. Nếu một mục tiêu phản hồi nhanh khiến pool hoàn thành các request nhanh hơn mức cho phép trong một phút, hãy điều tiết tốc độ của worker để cũng nằm dưới mức trần mỗi phút.

Bước 2: Back off thay vì gửi lại ngay

Nếu gặp từ chối, gửi lại toàn bộ batch ngay lập tức là cách xử lý sai. Mọi lệnh gọi trong đó sẽ lại bị từ chối tiếp, và các lượt thử lại sẽ dồn ứ lên trên các request vốn đang chạy.

Hãy đợi trước. Thời gian chờ nằm trong header Retry-After và trong retry_after_seconds ở phần 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"}

Thêm jitter khi bạn chạy nhiều worker. Nếu không có nó, mọi worker bị từ chối trong cùng một giây sẽ thức dậy cùng lúc trong giây tiếp theo và lại tiếp tục bị từ chối cùng nhau.

import random
time.sleep(wait + random.uniform(0, 0.5))

Bước 3: Thực hiện tương tự trong 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;
}

Số lượng worker cố định lấy tác vụ từ một hàng đợi sẽ duy trì chính xác bấy nhiêu lệnh gọi mở, bất kể kích thước batch.

Bước 4: Theo dõi mức sử dụng thực tế

Mở Usage & Limits trong dashboard. Bộ đếm trực tiếp bên cạnh các giới hạn của bạn sẽ hiển thị concurrency, tốc độ mỗi phút và browser requests mỗi ngày theo thời gian thực, giúp bạn định cỡ MAX_IN_FLIGHT dựa trên lần chạy thực tế thay vì phỏng đoán.

Activity Log cũng ghi lại các yêu cầu bị từ chối. Một lần chạy hoàn thành với đúng số lượng dòng nhưng có rải rác kết quả rate_limit nghĩa là đang tự gây nghẽn (throttling).

Các lỗi thường gặp

  • Gửi toàn bộ danh sách cùng một lúc. asyncio.gather trên 5.000 URL, hoặc Promise.all trên một mảng không giới hạn, sẽ mở 5.000 lệnh gọi. Hãy giới hạn worker pool, không phải danh sách.
  • Thử lại song song. Việc gửi lại mọi lệnh gọi bị từ chối ngay thời điểm bị từ chối sẽ tái tạo lại đợt bùng phát lưu lượng gây ra lỗi đó. Hãy chờ Retry-After và thêm jitter.
  • Coi hạn mức hàng ngày hoặc chu kỳ như một khoảng thời gian chờ. plan_limit_browser_daily được làm mới vào nửa đêm theo giờ UTC, plan_limit_creditsplan_limit_bandwidth vào cuối chu kỳ thanh toán. Hãy dừng lần chạy và đọc resets_at từ body nếu có.
  • Tính mỗi lệnh gọi auto là một slot. Bậc thang xử lý bên trong /api/auto/ tạo ra các sub-call thực tế, và đó là những gì được tính vào giới hạn trần.
  • Dùng chung một key giữa các process mà không phân chia ngân sách. Các giới hạn trần được tính theo tài khoản, không phải theo process.
  • Định cỡ một pool duy nhất cho mọi endpoint. Single, Proxy và Browser đều có số lượng concurrency riêng. Pool được định cỡ cho Single sẽ vượt quá giới hạn trần nhỏ hơn của Browser.

Liên quan

  • Rate Limits: Mọi dạng từ chối và ý nghĩa của từng trường
  • Response Headers: X-FourA-LimitRetry-After
  • Request Outcomes: Tại sao kết quả rate_limit không bao giờ bị tính phí
  • Usage & Limits: Nơi xem các bộ đếm trực tiếp và các thông số gói của bạn
  • Smart Fetch (Auto): Cách một lệnh gọi auto chuyển thành nhiều sub-call
Cập nhật: 10 tháng 9, 2026