Ocriva Logo

Documents

การ Retry และ Rate Limiting

ทำความเข้าใจ Rate Limits ของ API กลยุทธ์การ Retry และวิธีจัดการ 429 responses

rate-limitingretry429backoffapibest-practices

Published: 4/4/2026

การ Retry และ Rate Limiting

Ocriva API บังคับใช้ Rate Limits เพื่อให้แน่ใจว่าผู้ใช้ทุกคนได้รับการใช้งานที่เป็นธรรมและประสิทธิภาพที่เสถียร เมื่อแอปพลิเคชันของคุณส่ง request เกินขีดจำกัด API จะส่งกลับ 429 Too Many Requests คู่มือนี้อธิบาย Rate Limits, header ของ response ที่คุณสามารถตรวจสอบได้ และกลยุทธ์การ Retry ที่ควรนำไปใช้


ภาพรวม Rate Limits

ขอบเขตขีดจำกัด
Request ต่อนาที60 ต่อ API key
ไฟล์ต่อ Batch request50 ไฟล์
Concurrent connectionsไม่จำกัด (อยู่ภายใต้ขีดจำกัดต่อนาที)

Rate Limits ถูกติดตาม ต่อ API key ไม่ใช่ต่อ IP address หรือต่อ organization หากคุณมีหลาย service ที่ใช้ key เดียวกัน จำนวน request รวมของทุก service จะถูกนับรวมกันภายใต้ quota 60 req/min เดียวกัน ควรใช้ API key แยกกันสำหรับแต่ละ service เพื่อแยก limit ของแต่ละตัวออกจากกัน

หน้าต่างของ limit เป็นช่วงหนึ่งนาทีแบบ fixed ที่ reset ตามนาฬิกา (เช่น 09:00:00 → 09:01:00) Request จะถูกนับ ณ เวลาที่ไปถึง server หากส่ง request 60 ครั้งพร้อมกันที่เวลา 09:00:59 จะใช้ quota หมดทั้งช่วง; request ถัดไปจะได้รับอนุญาตที่เวลา 09:01:00

NOTE

Credits และ Rate Limits เป็นอิสระต่อกัน การใช้ credit balance หมดจะเกิด error คนละประเภท (402 Payment Required หรือ 400 ที่เกี่ยวกับ credit) ไม่ใช่ 429 ควรตรวจสอบทั้งสองอย่างแยกกันเสมอ


Rate Limit Headers

ทุก API response มี header ที่บอกสถานะ quota ปัจจุบันของคุณ ควรอ่าน header เหล่านี้อย่างเชิงรุก แทนที่จะรอให้เกิด 429

Headerประเภทคำอธิบาย
X-RateLimit-Limitnumberจำนวน request สูงสุดที่อนุญาตในช่วงเวลาปัจจุบัน (เสมอเป็น 60)
X-RateLimit-Remainingnumberจำนวน request ที่เหลืออยู่ในช่วงเวลาปัจจุบัน
X-RateLimit-ResetnumberUnix timestamp (วินาที) ที่ช่วงเวลาปัจจุบันจะ reset
Retry-Afternumberเฉพาะใน 429 responses เท่านั้น จำนวนวินาทีที่ต้องรอก่อน retry

ตัวอย่าง response header หลังจาก request สำเร็จ:

HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1743750060

ตัวอย่าง header บน 429 response:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1743750060
Retry-After: 23

Retry-After คือสัญญาณที่เชื่อถือได้ที่สุด บอกจำนวนวินาทีที่แน่นอนจนกว่าหน้าต่างจะ reset — ควรใช้ค่านี้เสมอแทนการคำนวณจาก X-RateLimit-Reset เอง


การจัดการ 429 Responses

เมื่อเกิน Rate Limit API จะตอบกลับด้วย HTTP 429 Too Many Requests พร้อม JSON body:

{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. You have sent 60 requests in the current minute. Please wait and retry."
}

429 คือ error ชั่วคราว — ไม่ได้บ่งบอกถึงปัญหากับ request ของคุณ request เดิมจะสำเร็จเมื่อหน้าต่างเวลา reset โค้ดของคุณต้องไม่ทิ้ง request เมื่อเกิด 429 ควร queue หรือ retry แทน

สถานการณ์ที่มักทำให้เกิด 429:

  • อัปโหลดไฟล์ในลูปถี่โดยไม่มีหน่วงเวลาระหว่าง request
  • รัน worker หลายตัวพร้อมกันโดยใช้ API key เดียวกัน
  • Polling status endpoint ทุกวินาทีแทนการใช้ webhook หรือ exponential backoff
  • ส่งการอัปโหลดแยกหลายรายการที่ควรจะเป็น batch operation เดียว

กลยุทธ์การ Retry

Exponential Backoff with Jitter

Exponential Backoff จะเพิ่มเวลาหน่วงเป็นสองเท่าหลังจากแต่ละความพยายามที่ล้มเหลว การเพิ่ม Jitter แบบสุ่มช่วยป้องกันไม่ให้ client หลายตัว retry พร้อมกันและขยาย load spike ("thundering herd" problem)

สูตรคำนวณ delay ที่แนะนำ:

delay = min(base * 2^attempt, cap) + random(0, jitter)

ค่าพารามิเตอร์เริ่มต้นที่ Ocriva SDK ใช้:

พารามิเตอร์ค่า
Base delay1 วินาที
Cap30 วินาที
Jitter range0–100 ms
Max retries3

เวลาหน่วงที่มีผลก่อนแต่ละครั้งที่ retry:

ครั้งที่Base delayพร้อม Jitter (โดยประมาณ)
retry ครั้งที่ 11 วินาที1.0–1.1 วินาที
retry ครั้งที่ 22 วินาที2.0–2.1 วินาที
retry ครั้งที่ 34 วินาที4.0–4.1 วินาที

การ Implement เอง (TypeScript)

ใช้ pattern นี้เมื่อคุณเรียก API โดยตรงโดยไม่ใช้ SDK หรือเมื่อต้องการ retry logic แบบกำหนดเอง:

interface RetryOptions {
  maxRetries?: number;
  baseDelayMs?: number;
  capMs?: number;
  jitterMs?: number;
}
 
async function fetchWithRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {},
): Promise<T> {
  const {
    maxRetries = 3,
    baseDelayMs = 1_000,
    capMs = 30_000,
    jitterMs = 100,
  } = options;
 
  let lastError: unknown;
 
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error: unknown) {
      lastError = error;
 
      if (!isRetryable(error) || attempt === maxRetries) {
        throw error;
      }
 
      const retryAfterMs = extractRetryAfter(error);
      const backoff = Math.min(baseDelayMs * 2 ** attempt, capMs);
      const jitter = Math.random() * jitterMs;
      const delay = retryAfterMs ?? backoff + jitter;
 
      console.warn(
        `Request failed (attempt ${attempt + 1}/${maxRetries}). ` +
          `Retrying in ${Math.round(delay)}ms...`,
      );
 
      await sleep(delay);
    }
  }
 
  throw lastError;
}
 
function isRetryable(error: unknown): boolean {
  if (error instanceof Response) return false;
  if (typeof error === 'object' && error !== null && 'status' in error) {
    const status = (error as { status: number }).status;
    // Retry on 429 and 5xx only — never retry 4xx client errors
    return status === 429 || status >= 500;
  }
  // Retry network-level errors (no status code)
  return true;
}
 
function extractRetryAfter(error: unknown): number | undefined {
  if (typeof error === 'object' && error !== null && 'headers' in error) {
    const headers = (error as { headers: Headers }).headers;
    const value = headers?.get?.('Retry-After');
    if (value) {
      const seconds = parseFloat(value);
      if (!Number.isNaN(seconds)) return seconds * 1_000;
    }
  }
  return undefined;
}
 
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

การใช้งาน:

const result = await fetchWithRetry(
  () =>
    fetch('https://api.ocriva.com/upload/YOUR_ORG_ID', {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.OCRIVA_API_KEY}` },
      body: formData,
    }).then((res) => {
      if (!res.ok) throw Object.assign(new Error('Request failed'), { status: res.status, headers: res.headers });
      return res.json();
    }),
  { maxRetries: 3, baseDelayMs: 1_000 },
);

IMPORTANT

ควรกำหนด จำนวนครั้ง retry สูงสุดเสมอ การ retry ไม่จำกัดครั้งระหว่าง outage ที่ต่อเนื่องจะทำให้ quota Rate Limit หมดเร็วขึ้นและชะลอการกู้คืนระบบ ค่าแนะนำคือ retry 3 ครั้งพร้อม backoff

การปฏิบัติตาม Retry-After Header

เมื่อ 429 response มี Retry-After header ควรรออย่างน้อยเท่ากับจำนวนวินาทีนั้นเสมอ แม้ว่าสูตร backoff ของคุณจะแนะนำให้รอน้อยกว่า ค่า Retry-After เป็นค่าที่เชื่อถือได้ที่สุด: การ retry ก่อนที่เวลานั้นจะหมดจะทำให้เกิด 429 อีกครั้งเกือบแน่นอน

// Prefer Retry-After over calculated backoff when available
const retryAfterHeader = response.headers.get('Retry-After');
const waitMs = retryAfterHeader
  ? parseFloat(retryAfterHeader) * 1_000
  : baseDelayMs * 2 ** attempt;
 
await sleep(waitMs);

SDK Automatic Retries

หากคุณใช้ @ocriva/sdk การ retry จะถูกจัดการโดยอัตโนมัติ SDK จะดักจับ 429 และ 5xx responses รอเวลาที่เหมาะสม และส่ง request ใหม่อย่างโปร่งใส

import { OcrivaClient } from '@ocriva/sdk';
 
const client = new OcrivaClient({
  apiKey: process.env.OCRIVA_API_KEY!,
  maxRetries: 3,   // default; set to 0 to disable
  timeout: 30_000, // per-request timeout in ms
});
Optionค่าเริ่มต้นคำอธิบาย
maxRetries3จำนวนครั้งที่ retry หลังจาก request แรกล้มเหลว
timeout30000มิลลิวินาทีก่อนที่ request จะ timeout และถูก retry

เมื่อ SDK ได้รับ 429 ที่มี Retry-After header จะใช้ค่านั้นแทน exponential backoff เมื่อ retry ครบทุกครั้งแล้ว SDK จะ throw RateLimitError พร้อม property retryAfter:

import { RateLimitError } from '@ocriva/sdk';
 
try {
  await client.upload.create(orgId, formData);
} catch (error) {
  if (error instanceof RateLimitError) {
    console.error(
      `Rate limit exhausted after all retries. ` +
        `Try again in ${error.retryAfter ?? 'a moment'} second(s).`,
    );
  }
}

สำหรับรายละเอียด error class ทั้งหมดและ error hierarchy ที่ครบถ้วน ดูที่ การจัดการ Error


Batch Operations

วิธีที่มีประสิทธิภาพสูงสุดในการลดจำนวน request คือการใช้ batch endpoint แทนการอัปโหลดทีละไฟล์ Batch ที่มี 50 ไฟล์ใช้ API request เพียงครั้งเดียวแทนที่จะเป็น 50 ครั้ง

วิธีการAPI requests สำหรับ 50 ไฟล์
อัปโหลดแยกทีละไฟล์ (loop)50
อัปโหลด batch เดียว1
// Avoid: 50 individual requests, each consuming rate limit quota
for (const file of files) {
  await client.upload.create(orgId, buildFormData(file)); // one request per file
}
 
// Prefer: one batch request regardless of file count (up to 50)
const batchForm = new FormData();
for (const file of files) {
  batchForm.append('files', file);
}
batchForm.append('projectId', projectId);
batchForm.append('templateId', templateId);
 
await client.batch.upload(orgId, batchForm); // single request

สำหรับรายละเอียด batch API ทั้งหมด การติดตาม progress และ export options ดูที่ Batch Processing

TIP

เมื่อมีไฟล์มากกว่า 50 ไฟล์ ให้แบ่งเป็นหลาย batch ขนาด 50 ไฟล์ แม้การส่ง batch หลาย request ก็ยังมีประสิทธิภาพกว่าการอัปโหลดทีละไฟล์หลายร้อยครั้ง และแต่ละ batch สามารถส่งโดยเพิ่มหน่วงเวลาสั้นๆ ระหว่างกัน


Idempotency

เมื่อคุณ retry request หลังจากเกิด 429 หรือ network error คุณเสี่ยงที่จะส่ง operation เดิมซ้ำสองครั้ง ตัวอย่างเช่น อัปโหลดไฟล์เดิมซ้ำหาก request แรกสำเร็จจริงแต่ response สูญหาย ควรออกแบบโค้ดและการใช้ API ให้เป็น idempotent

Webhook Event Idempotency

Webhook อาจถูกส่งมาซ้ำมากกว่าหนึ่งครั้ง ควรใช้ฟิลด์ eventId ใน webhook payload เป็น idempotency key เสมอก่อนประมวลผล:

const processedEvents = new Set<string>();
 
app.post('/webhook', (req, res) => {
  const { eventId, event, data } = req.body;
 
  if (processedEvents.has(eventId)) {
    // Already handled — acknowledge without re-processing
    return res.status(200).json({ received: true });
  }
 
  processedEvents.add(eventId);
 
  // Safe to process
  handleWebhookEvent(event, data);
 
  res.status(200).json({ received: true });
});

ในการใช้งาน production ควรเก็บค่า eventId ที่ประมวลผลแล้วในฐานข้อมูล (เช่น Redis หรือ MongoDB) พร้อม TTL อย่างน้อย 24 ชั่วโมง แทนการเก็บไว้ใน memory

การออกแบบ API Consumer แบบ Idempotent

  • ตรวจสอบผลลัพธ์ที่มีอยู่ก่อนอัปโหลดซ้ำ Query Processing History ด้วย filename หรือ external reference ก่อน resubmit เอกสาร
  • ใช้ชื่อ batch ที่เสถียร การตั้งชื่อ batch ด้วย key แบบ deterministic (เช่น "invoices-2026-03") ทำให้ตรวจจับได้ง่ายว่า batch ถูกส่งไปแล้วหรือไม่
  • ติดตาม upload ID เก็บ id ที่ได้รับจาก upload หรือ batch endpoint หาก job runner ของคุณ restart ให้ตรวจสอบว่า ID นั้นมีอยู่แล้วก่อนสร้าง request ใหม่

แนวทางปฏิบัติที่ดี

  • ตรวจสอบ credit balance ก่อน batch ขนาดใหญ่ Batch 50 ไฟล์จะหัก 50 credits ทันที การเกิด 402 กลางคัน batch จะสูญเสีย quota สำหรับไฟล์ที่ประมวลผลไปแล้ว ดึงยอด balance จาก credits endpoint ก่อนส่ง
  • ใช้ batch endpoint แทนการอัปโหลดแยก Batch request เดียวที่มี 50 ไฟล์ใช้ quota Rate Limit หนึ่งหน่วย การอัปโหลด 50 ไฟล์เดียวกันทีละครั้งใช้ 50 หน่วย
  • เพิ่มหน่วงเวลาในลูปที่ทำงานถี่ หากต้องส่ง request หลายรายการตามลำดับ (เช่น หลาย batch) ให้เพิ่ม sleep สั้นๆ ระหว่างกัน:
    for (const chunk of chunks) {
      await client.batch.upload(orgId, buildBatchForm(chunk));
      await sleep(1_000); // หยุด 1 วินาทีช่วยให้อยู่ต่ำกว่า 60 req/min ได้สบายๆ
    }
  • ตรวจสอบ Rate Limit headers อย่างเชิงรุก อย่ารอให้เกิด 429 ก่อน อ่าน X-RateLimit-Remaining จากทุก response หากลดลงต่ำกว่า 10 ให้ชะลอความเร็วโดยสมัครใจ
  • ใช้ API key แยกต่างหากสำหรับแต่ละ service แต่ละ key มี quota อิสระของตัวเอง หากคุณมี background job และ user-facing API ที่ใช้ key เดียวกัน การรัน background งานหนักๆ ครั้งเดียวอาจบล็อก user ของคุณได้
  • ใช้ webhook แทนการ polling การ poll status endpoint ทุกวินาทีเป็นวิธีที่เร็วที่สุดในการใช้ 60 req/min หมด ให้ subscribe กับ event batch.completed หรือ upload.completed และประมวลผลแบบ asynchronous แทน

ข้อผิดพลาดที่พบบ่อย

Polling โดยไม่มี Backoff

// Wrong: hammers the API every second, exhausts quota in one minute
while (true) {
  const status = await client.processingHistory.get(id, projectId);
  if (status.status === 'completed') break;
  await sleep(1_000); // 1 second — 60 polls per minute = instant rate limit
}
 
// Correct: exponential backoff or use webhooks
let delay = 2_000;
while (true) {
  const status = await client.processingHistory.get(id, projectId);
  if (status.status === 'completed') break;
  await sleep(delay);
  delay = Math.min(delay * 2, 30_000); // back off up to 30 s
}

ไม่สนใจ Retry-After Header

// Wrong: uses a hardcoded delay that may be shorter than the server requires
} catch (error) {
  await sleep(500); // server said wait 23 s; this will fail again immediately
  return retry();
}
 
// Correct: always honour Retry-After
} catch (error) {
  if (error instanceof RateLimitError && error.retryAfter) {
    await sleep(error.retryAfter * 1_000);
  }
}

ไม่มีจำนวน Retry สูงสุด

// Wrong: retries forever — stalls the process and wastes quota during an outage
async function upload(data: FormData): Promise<unknown> {
  try {
    return await client.upload.create(orgId, data);
  } catch {
    return upload(data); // infinite recursion on persistent errors
  }
}
 
// Correct: bounded retries with backoff
const result = await fetchWithRetry(() => client.upload.create(orgId, data), {
  maxRetries: 3,
});

WARNING

Infinite retry loop ระหว่าง API outage จะทำให้ Rate Limit window ของคุณถูกใช้จนเต็มตลอดช่วงเวลาที่เกิด outage ทำให้ request ที่สำเร็จจากส่วนอื่นของแอปพลิเคชันไม่สามารถผ่านได้เลย