Chạy các request song song

Nội dung bạn sẽ học

Cách chạy đồng thời một lượng lớn FourA request 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ì lặp lại yêu cầu.

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

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

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

Gói dịch vụ của bạn áp dụng hai mức trầ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ê tại tab Limits & Features trong Usage & Limits.

Request vượt quá giới hạn đồng thời sẽ trả về mã HTTP 429 kèm X-FourA-Limit: plan_limit_concurrency và thông tin mức trầ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_rate, và retry_after_seconds cho biết cần đợi bao lâu để có thể gửi thêm một request (từ 1 đến 120 giây).

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ý sau, do đó không tiêu tốn tài nguyên và không tính phí. Một lệnh gọi bị từ chối sẽ không tính vào khoảng thời gian một phút trượt (sliding minute), vì vậy tài khoản gửi yêu cầu vượt nhẹ mức trần vẫn tận dụng được toàn bộ hạn mức. Tình trạng bão gửi lại (retry storm) có cơ chế xử lý riêng: khi các lệnh gọi bị từ chối tiếp tục xuất hiện với tần suất lớn hơn gấp đôi mức trần mỗi phút của bạn, mọi lệnh gọi đến endpoint đó sẽ bị từ chối kèm cooldown: true và retry_after_seconds: 30, hãy tạm dừng trong 30 giây này. Tài liệu tham khảo đầy đủ về các trường: Rate Limits.

Hai yếu tố quan trọng thường bị bỏ qua:

  • Bản thân lệnh gọi POST /api/auto/ không chiếm slot, nhưng mỗi lệnh gọi con Single, Proxy và Browser mà nó thực hiện thay bạn thì có. Một lệnh gọi auto có thể chiếm nhiều hơn một slot trong quá trình ladder hoạt động. (Trên dashboard của bạn, lệnh gọi auto được tính là request; các lệnh gọi con là các attempts.)
  • Một Browser request chiếm slot của nó trong toàn bộ thời gian trang kết xuất, lâu hơn nhiều so với Single request. Một nhóm lệnh gọi browser sẽ chạm trần hạn mức với số lượng request ít hơn so với một nhóm lệnh gọi single.

Bước 1: Giới Hạn Mức Đồng Thời Của Chính Bạn

Hãy chọn một con số thấp hơn mức trần của endpoint bạn đang gọi và duy trì nó. 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 trong 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 tài khoản, ngay cả khi dùng API key khác nhau, chúng vẫn 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 tất các request nhanh hơn mức cho phép trong một phút, hãy điều chỉnh tốc độ của worker để duy trì dưới mức trần mỗi phút.

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

Nếu xuất hiện thông báo từ chối, giải pháp sai lầm là gửi lại toàn bộ batch ngay lập tức. Mọi lệnh gọi trong đó sẽ tiếp tục bị từ chối, và các lần thử lại sẽ dồn ứ lên các request đang chạy.

Hãy đợi trước. Thời gian chờ được ghi trong header Retry-After và trong retry_after_seconds ở 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 đang 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 trong cùng một giây và cùng bị từ chối tiếp.

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;
}

Một số lượng worker cố định lấy tác vụ từ một hàng đợi duy nhất sẽ giữ 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 mức giới hạn hiển thị concurrency, rate mỗi phút, và browser request mỗi ngày theo thời gian thực, giúp bạn định kích thướ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 lần từ chối. Một lần chạy kết thúc với đủ số dòng nhưng rải rác các kết quả rate_limit nghĩa là đã tự gây nghẽn.

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. Gửi lại mọi lệnh gọi bị từ chối ngay thời điểm nó bị từ chối sẽ lặp lại tình trạng đột biến lưu lượng đã gây ra lỗi. Hãy đợi theo Retry-After, và thêm jitter.
  • Xem hạn ngạch ngày hoặc chu kỳ như một khoảng thời gian chờ ngắn. plan_limit_browser_daily làm mới vào nửa đêm UTC, plan_limit_credits và plan_limit_bandwidth làm mới vào cuối chu kỳ thanh toán. Hãy dừng lần chạy và đọc resets_at từ body phản hồi nếu có.
  • Tính mỗi lệnh gọi auto là một slot. Cơ chế bậc thang bên trong /api/auto/ tạo ra các sub-call thực tế, và đó là những gì các mức giới hạn trần tính vào.
  • Dùng chung một key giữa nhiều tiến trình mà không chia ngân sách. Các mức giới hạn trần được tính theo tài khoản, không phải theo tiến trình.
  • Dùng chung một kích thước pool cho mọi endpoint. Single, Proxy và Browser đều có số concurrency riêng. Một pool có kích thước phù hợp với Single sẽ vượt quá giới hạn trần nhỏ hơn của Browser.
  • Rate Limits: Mọi dạng từ chối và ý nghĩa từng trường
  • Response Headers: X-FourA-Limit và Retry-After
  • Request Outcomes: Vì sao kết quả rate_limit không bao giờ bị tính phí
  • Usage & Limits: Nơi hiển thị 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: 15 tháng 10, 2026