ขีดจำกัดการเรียก
มีสองขีดจำกัดที่ปกป้องระบบ และนับแยกกัน คือขีดจำกัดต่อนาที กับโควตาต่อวัน ทั้งคู่นับแยกรายกุญแจ กุญแจที่ออกให้ระบบคนละตัวจึงไม่แย่งโควตากันเอง
ขีดจำกัด
หัวข้อที่มีชื่อว่า “ขีดจำกัด”| ขีดจำกัด | ค่าตั้งต้น | หน้าต่างเวลา | นับกับ |
|---|---|---|---|
| ต่อนาที | 60 คำขอต่อนาที | 60 วินาทีแบบเลื่อนไปเรื่อย ๆ | กุญแจแต่ละใบ |
| ต่อวัน | 10,000 คำขอต่อวัน | วันตามปฏิทินไทย รีเซ็ต 00:00 น. เวลาไทย | กุญแจแต่ละใบ |
นี่คือค่าตั้งต้นที่กุญแจทุกใบเริ่มต้น และปรับขึ้นหรือลงรายกุญแจได้ตอนออกใบ
โควตาต่อวันที่ใช้จริงกับกุญแจของคุณจะถูกส่งกลับมาในทุกคำขอที่สำเร็จ ที่ส่วนหัว X-RateLimit-Limit
ให้อ่านจากตรงนั้นแทนการเขียนตัวเลขตายไว้ในโค้ด ส่วนเพดานต่อนาทีจะถูกรายงานเป็นช่อง limit
ใน body ของคำตอบ 429 rate_limited
สองรายละเอียดที่ตัดสินว่าโควตาพอหรือไม่
- นับคำขอที่เข้ามา ไม่ใช่คำขอที่สำเร็จ คำขอที่ล้มซ้ำ ๆ ก็กินโควตา
ถ้าเจอลูปที่ได้
400ให้แก้ ไม่ใช่ปล่อยให้วิ่งต่อ - ตัวนับรายวันรีเซ็ตเที่ยงคืนเวลาไทย (UTC+7) ไม่ใช่เที่ยงคืนตามเขตเวลาของคุณ งานที่เริ่ม 23:00 น. ในยุโรป กำลังใช้โควตาของวันถัดไปอยู่แล้ว
ส่วนหัวที่บอกโควตา
หัวข้อที่มีชื่อว่า “ส่วนหัวที่บอกโควตา”| ส่วนหัว | ติดมากับคำตอบไหน | ค่า |
|---|---|---|
X-RateLimit-Limit |
ทุกคำขอที่ผ่านการตรวจกุญแจและผ่านขีดจำกัดต่อนาทีแล้ว | โควตา ต่อวัน ของกุญแจใบนั้น |
X-RateLimit-Remaining |
เช่นเดียวกัน | จำนวนคำขอที่เหลือของวันนี้ หลังหักคำขอนี้แล้ว |
Retry-After |
เฉพาะ 429 rate_limited |
จำนวนวินาทีที่ควรรอก่อนลองใหม่ |
ดูยอดใช้ของตัวเอง
หัวข้อที่มีชื่อว่า “ดูยอดใช้ของตัวเอง”ส่วนหัวข้างบนบอกสถานะของคำขอที่เพิ่งยิงไป และติดมาเฉพาะคำขอที่มีมันเท่านั้น
ส่วน GET /v1/usage ตอบคำถามเดียวกันแบบตรง ๆ
เมื่อไหร่ก็ได้ที่ถาม ทั้งเพดานที่กุญแจของคุณใช้จริง ยอดที่ใช้ไปแล้ววันนี้ และอีกหกวันก่อนหน้า
เส้นนี้ไม่รับพารามิเตอร์ใด ๆ รายงานเฉพาะกุญแจที่อยู่ในส่วนหัว Authorization
เท่านั้น ไม่มีทางถามยอดของกุญแจใบอื่น
curl -s -H "Authorization: Bearer $SCANFOOD_API_KEY" \ "https://api.scanfood.co/ext/v1/usage"const res = await fetch('https://api.scanfood.co/ext/v1/usage', { headers: { Authorization: `Bearer ${process.env.SCANFOOD_API_KEY}` },});const { data } = await res.json();console.log(`ใช้ไป ${data.today.count} เหลือวันนี้อีก ${data.today.remaining}`);r = requests.get("https://api.scanfood.co/ext/v1/usage", headers=HEADERS, timeout=30)r.raise_for_status()usage = r.json()["data"]print(f"ใช้ไป {usage['today']['count']} เหลือวันนี้อีก {usage['today']['remaining']}"){ "ok": true, "data": { "keyId": "9f3a1c7d5b2e", "label": "ซิงก์คลังรอบดึก", "scope": "shop", "rate": { "perMin": 60, "perDay": 10000 }, "today": { "date": "20260910", "count": 127, "remaining": 9873 }, "days": [ { "date": "20260904", "count": 96 }, { "date": "20260905", "count": 101 }, { "date": "20260906", "count": 0 }, { "date": "20260907", "count": 88 }, { "date": "20260908", "count": 143 }, { "date": "20260909", "count": 132 }, { "date": "20260910", "count": 127 } ] }, "serverTime": "2026-09-10T06:45:12.310Z"}| ช่อง | หมายความว่า |
|---|---|
keyId |
กุญแจที่ใช้ยืนยันตัวตน คือส่วนหน้าของกุญแจที่อยู่ก่อนท่อนลับ |
label |
ป้ายชื่อที่บันทึกไว้ตอนออกกุญแจ ถ้าไม่ได้ตั้งไว้จะเป็นสตริงว่าง |
scope |
shop คือร้านเดียว · franchise คือทุกสาขาภายใต้แฟรนไชส์เดียว |
rate.perMin |
เพดานต่อนาทีที่กุญแจใบนี้ใช้จริง |
rate.perDay |
โควตาต่อวันที่กุญแจใบนี้ใช้จริง เลขเดียวกับ X-RateLimit-Limit |
today.date |
วันนี้ตามเวลาไทย รูป YYYYMMDD |
today.count |
จำนวนคำขอที่นับไปแล้ววันนี้ รวมคำขอนี้ด้วย |
today.remaining |
perDay ลบ count และไม่ต่ำกว่าศูนย์ |
days[] |
เจ็ดวันไทยล่าสุด เก่าสุดอยู่ต้น วันนี้อยู่ท้าย แต่ละตัวมี date กับ count |
สามข้อที่ควรรู้ก่อนเอาตัวเลขไปใช้ต่อ
today.countรวมคำขอที่เพิ่งยิงไปด้วย เพราะนับตอนคำขอเข้ามา ดังนั้นtoday.remainingจึงเท่ากับส่วนหัวX-RateLimit-Remainingของคำตอบเดียวกันนั้นพอดี และการถามยอดใช้ก็กินโควตาหนึ่งคำขอเหมือนการเรียกเส้นอื่นdaysมีเจ็ดรายการเสมอ วันที่ไม่มีการเรียกเลยจะคืนมาเป็น0ไม่ใช่หายไปจากลิสต์ กราฟหรือรายงานจึงไม่ต้องมาเติมช่องว่างเอง- นี่คือที่เดียวที่บอก
perMinได้โดยไม่ต้องไปชนมัน ไม่มีส่วนหัวใดบอกเพดานต่อนาที นอกจากทางนี้ก็เหลือแค่ช่องlimitใน body ของคำตอบ429 rate_limited
รับมือกับ 429
หัวข้อที่มีชื่อว่า “รับมือกับ 429”สถานะ 429 ใช้ร่วมกันสองสถานการณ์ ซึ่งต้องรับมือคนละแบบ ให้แตกสาขาที่ช่อง error
error |
หมายความว่า | ทำอย่างไร |
|---|---|---|
rate_limited |
ยิงถี่เกินไปในหนึ่งนาทีที่ผ่านมา | รอตาม Retry-After วินาที แล้วลองใหม่ อย่าลดเวลารอ ใน body ยังมี retryAfterSec limit และ windowSec |
quota_exceeded |
โควตาของวันหมดแล้ว | หยุดรอบนี้ไปก่อนจนถึงวันถัดไป หรือขอเพิ่มโควตา ลองใหม่ก่อน 00:00 น. เวลาไทย ไม่ช่วยอะไร |
{ "ok": false, "error": "rate_limited", "message": "Too many requests, retry in about 37 seconds", "retryAfterSec": 37, "limit": 60, "windowSec": 60}ตัวช่วยลองใหม่ที่รองรับทั้งสองกรณี
# ปล่อยให้ curl เป็นคนรอและลองใหม่ให้curl -s --retry 5 --retry-delay 5 --retry-all-errors \ -H "Authorization: Bearer $SCANFOOD_API_KEY" \ "https://api.scanfood.co/ext/v1/shops"async function call(url, attempt = 0) { const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SCANFOOD_API_KEY}` }, }); if (res.status === 429) { const body = await res.json(); if (body.error === 'quota_exceeded') throw new Error('โควตารายวันหมดแล้ว'); const wait = Number(res.headers.get('retry-after') ?? body.retryAfterSec ?? 30); await new Promise((r) => setTimeout(r, wait * 1000)); return call(url, attempt + 1); } if (res.status >= 500 && attempt < 4) { await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); return call(url, attempt + 1); } return res.json();}import time
def call(url, params=None, attempt=0): r = requests.get(url, headers=HEADERS, params=params, timeout=30) if r.status_code == 429: body = r.json() if body.get("error") == "quota_exceeded": raise RuntimeError("โควตารายวันหมดแล้ว") wait = int(r.headers.get("Retry-After") or body.get("retryAfterSec") or 30) time.sleep(wait) return call(url, params, attempt + 1) if r.status_code >= 500 and attempt < 4: time.sleep(2 ** attempt) return call(url, params, attempt + 1) r.raise_for_status() return r.json()ออกแบบรอบการดึงข้อมูล
หัวข้อที่มีชื่อว่า “ออกแบบรอบการดึงข้อมูล”ขีดจำกัดนี้เหลือเฟือสำหรับการซิงก์ แต่คับสำหรับการไล่ดูดทั้งฐาน คิดเลขสักหน่อยก่อนเขียนตัวตั้งเวลา
- ขอทีละหน้าเต็ม
limit=200คือค่าสูงสุด วันที่มี 600 บิล ใช้ 4 คำขอถ้าขอ 200 แต่ใช้ 7 คำขอถ้าขอ 100 คือหน้าเต็ม 3 หน้า (หรือ 6 หน้า) บวกอีกหนึ่งคำขอปิดท้าย เพราะหน้าที่กลับมาเต็มพอดีต้องยิงอีกหนึ่งครั้งจึงจะเห็นnextCursor: null - หนึ่งคำขอต่อหนึ่งร้าน แฟรนไชส์ 20 สาขา ดึงทุก 15 นาที เท่ากับ
20 × 4 × 24 = 1,920คำขอต่อวันก่อนนับการแบ่งหน้า ซึ่งยังสบายใน 10,000 แต่กลุ่มที่ใหญ่กว่านี้ควรคิดเลขก่อน - ดึงเฉพาะส่วนที่เปลี่ยน
updatedSinceคืนเฉพาะสิ่งที่เปลี่ยนตั้งแต่รอบก่อน ช่วงสิบห้านาทีที่ร้านเงียบจึงเสียแค่คำขอเดียวต่อร้าน - ดึงย้อนหลังอย่างมีแผน ประวัติดึงได้ครั้งละไม่เกิน 31 วัน หนึ่งปีต่อหนึ่งร้าน จึงอย่างน้อย 12 คำขอบวกการแบ่งหน้า ให้รันครั้งเดียวนอกเวลาเร่งด่วน ไม่ใช่รันทุกครั้งที่ deploy
- กระจายสาขาออกจากกัน ยิง 20 ร้านพร้อมกันในวินาทีเดียวคือการยิงถี่ แต่ 20 ร้านเดียวกันที่กระจายทั้งนาทีไม่ใช่
ถ้างานที่จำเป็นจริง ๆ ยังไม่พอ ให้ขอเพิ่มโควตา ดีกว่าแตกระบบเดียวไปใช้หลายกุญแจ เพราะกุญแจหลายใบทำให้ย้อนดูการใช้งานไม่ได้