การ 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 request | 50 ไฟล์ |
| 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-Limit | number | จำนวน request สูงสุดที่อนุญาตในช่วงเวลาปัจจุบัน (เสมอเป็น 60) |
X-RateLimit-Remaining | number | จำนวน request ที่เหลืออยู่ในช่วงเวลาปัจจุบัน |
X-RateLimit-Reset | number | Unix timestamp (วินาที) ที่ช่วงเวลาปัจจุบันจะ reset |
Retry-After | number | เฉพาะใน 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: 23Retry-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 delay | 1 วินาที |
| Cap | 30 วินาที |
| Jitter range | 0–100 ms |
| Max retries | 3 |
เวลาหน่วงที่มีผลก่อนแต่ละครั้งที่ retry:
| ครั้งที่ | Base delay | พร้อม Jitter (โดยประมาณ) |
|---|---|---|
| retry ครั้งที่ 1 | 1 วินาที | 1.0–1.1 วินาที |
| retry ครั้งที่ 2 | 2 วินาที | 2.0–2.1 วินาที |
| retry ครั้งที่ 3 | 4 วินาที | 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 | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|
maxRetries | 3 | จำนวนครั้งที่ retry หลังจาก request แรกล้มเหลว |
timeout | 30000 | มิลลิวินาทีก่อนที่ 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 ที่สำเร็จจากส่วนอื่นของแอปพลิเคชันไม่สามารถผ่านได้เลย
