ข้ามไปยังเนื้อหา

Rate Limiting and Throttling

ทุกบริการมีเพดาน instance หนึ่งสามารถรับ request ได้เพียงจำนวนหนึ่งต่อวินาทีก่อนที่ CPU จะอิ่มตัว connection pool เต็ม หรือหน่วยความจำพุ่งสูง ส่วนใหญ่คุณทำงานอยู่ต่ำกว่าเพดานนั้นอย่างสบาย ๆ แต่ traffic ไม่ราบเรียบ: อีเมลการตลาดถูกส่งออกไป client ยอดนิยม deploy retry loop ที่แน่นเกินไป scraper ค้นพบ API ของคุณ หรือเหตุล่มที่ต้นทางจู่ ๆ เปลี่ยนทิศทาง request จำนวนมหาศาลมาทางคุณ

pattern ก่อนหน้าในโมดูลนี้ปกป้องคุณจากการที่ dependency ของคุณ ล้มเหลว rate limiting ปกป้องคุณจาก ผู้เรียกของคุณ — และจากความสำเร็จของคุณเอง

พอ demand เกินกำลัง service ไม่ได้ค่อย ๆ ทรุดลงอย่างนุ่มนวล แต่คว่ำทั้งยวง เมื่อ request เข้ามาเร็วกว่าที่ทำเสร็จ คิวก็ยาวขึ้น latency พุ่ง timeout เริ่มทำงาน แล้ว client ก็ retry ซึ่งยิ่งเพิ่มภาระให้ service ที่แบกไม่ไหวอยู่แล้ว พ้นจุดพลิกผันไป throughput ไม่ได้แค่นิ่ง แต่ดิ่งลงเลย เพราะ server หมดเวลาไปกับการไล่เคลียร์ backlog ที่ไม่มีวันหมด service ที่รับได้สบาย ๆ 1,000 request ต่อวินาที อาจเหลือ ศูนย์ เมื่อเจอ 5,000 request เพราะทุก request timeout ก่อนจะทำเสร็จ

แรงที่ขัดกันจึงเป็นแบบนี้ คุณมีกำลังจำกัด demand พุ่งเกินได้แบบคาดเดาไม่ได้ และการรับ ทุกอย่างแต่ทำได้แย่ ย่อมแย่กว่าการรับ ส่วนใหญ่แล้วทำได้ดี คุณจึงต้องรักษาให้ service ทำงานอยู่ในกรอบที่ปลอดภัย แม้ตอนที่งานเข้ามามากกว่าที่รับไหว

rate limiting จำกัดปริมาณงานที่ service จะรับต่อหน่วยเวลา เพื่อคุมให้อยู่ในกรอบที่ปลอดภัย พอ request เข้ามาเร็วกว่าขีดจำกัดที่ตั้งไว้ ส่วนเกินจะโดน shed คือปฏิเสธทันที ปกติด้วย HTTP 429 Too Many Requests หรือ เข้าคิว ชั่วครู่เพื่อรีดช่วง burst สั้น ๆ ให้ราบ ไม่ว่าทางไหน งานที่รับเข้ามาแล้วจะได้รับบริการอย่างดี

กลไกที่ใช้กันมากที่สุดคือ token bucket ถังหนึ่งใบเก็บ token ได้สูงสุด capacity token และเติมกลับในอัตราคงที่ refillPerSecond token ทุก request ต้องหยิบ token หนึ่งตัวถึงจะทำงานได้ ถ้าถังว่างก็โดนปฏิเสธ เพราะถังสะสม token สำรองไว้ได้ จึงยอมให้เกิด burst สั้น ๆ โดยใช้ token ที่สะสมไว้ พร้อมกับยังคุมอัตราเฉลี่ยระยะยาวไว้ที่อัตรา refill การผสมสองอย่างนี้ คือยืดหยุ่นในระดับเล็กแต่มีเพดานในระดับใหญ่ ตรงกับพฤติกรรมจริงของ traffic พอดี

flowchart LR
  REF["Refill: +r tokens / second"] --> B
  subgraph B[Token bucket • capacity C]
    T[(tokens)]
  end
  REQ[Incoming request] --> CHK{token<br/>available?}
  B --> CHK
  CHK -- yes --> TAKE[take 1 token → allow]
  CHK -- no --> DROP[shed → HTTP 429]
token bucket — การ refill คงที่อนุญาตอัตราที่ยั่งยืน capacity ที่เก็บไว้ดูดซับ burst สั้น ๆ เมื่อว่างหมายถึง shed

ตัวอย่างนี้คือ limiter แบบ token bucket ที่ refill แบบ lazy ตามเวลาที่ผ่านไป แล้วรับ request ก็ต่อเมื่อมี token เหลือ แต่ละตัวอย่างรันได้ด้วยตัวเองและเขียนตามสไตล์ของภาษานั้น ๆ

class TokenBucket {
private tokens: number;
private last: number = Date.now();
constructor(
private readonly capacity: number,
private readonly refillPerSec: number,
) {
this.tokens = capacity;
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.last) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSec);
this.last = now;
}
tryAcquire(): boolean {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return true;
}
return false;
}
}
const limiter = new TokenBucket(100, 50); // burst 100, sustain 50/s
if (!limiter.tryAcquire()) {
return new Response('Too Many Requests', { status: 429 });
}

สิ่งที่คุณได้รับ:

  • กำลังของระบบได้รับการปกป้อง service จะไม่รับงานเกินกว่าที่ให้บริการไหว เวลาเจอโหลดหนักจึงค่อย ๆ ทรุดด้วยการ ปฏิเสธบาง request แทนที่จะ ล้มทั้งหมด throughput จะนิ่งอยู่ที่เพดานแทนที่จะดิ่งลงเมื่อพ้นจุดพลิกผัน
  • ความเป็นธรรมและการควบคุมการใช้งานในทางที่ผิด การจำกัดต่อ client (ต่อ API key, ต่อ IP) หยุดผู้เรียกที่เสียงดังหรือมุ่งร้ายตัวหนึ่งไม่ให้ทำให้คนอื่นทั้งหมดอดอยาก และให้คันโยกแก่คุณในการรับมือ retry loop ที่ควบคุมไม่ได้และ scraper
  • ความทนทานต่อ burst token bucket ดูดซับการพุ่งสูงสั้น ๆ ที่ถูกต้องตามกฎหมายโดยใช้ capacity ที่เก็บไว้ และยังกำหนดขอบเขตอัตราที่ยั่งยืนไว้ได้ — คุณไม่ต้องเลือกระหว่าง “ราบเรียบเท่านั้น” กับ “ไม่มีขีดจำกัด”

สิ่งที่คุณต้องจ่าย:

  • การเลือกขีดจำกัด ตั้งต่ำไปก็ปฏิเสธ traffic ที่ service รับไหวจริง ตั้งสูงไป limiter ก็ไม่ได้ปกป้องอะไร ตัวเลขต้องมาจาก load test กับกำลังจริง ไม่ใช่จากการเดา และต้องปรับใหม่ทุกครั้งที่ service เปลี่ยน
  • การ return สัญญาณที่ถูกต้อง request ที่ถูกปฏิเสธควร return 429 Too Many Requests ในอุดมคติพร้อม header Retry-After เพื่อให้ client ที่มีพฤติกรรมดีถอยกลับแทนที่จะกระหน่ำ error เปล่า ๆ เป็นการเชื้อเชิญให้ retry ทันทีซึ่งทำลายล้าง limiter
  • การประสานงานแบบกระจาย (distributed coordination) limiter ใน instance หนึ่งเห็นเฉพาะ traffic ของ instance นั้น การบังคับขีดจำกัดแบบ global ข้ามหลาย instance ต้องใช้สถานะที่แชร์กัน (central store เช่น Redis) ซึ่งเพิ่ม dependency และ latency บ้างใน hot path
  • Circuit Breaker — ปกป้อง คุณ จาก dependency ที่ล้มเหลว ส่วน rate limiting ปกป้อง service จาก ผู้เรียกเข้ามา
  • Bulkhead — จำกัดความ concurrency ส่วน rate limiting จำกัด throughput ทั้งสองประกอบกันได้ดี
  • Retry and Timeout429 พร้อม Retry-After บอกตรรกะการ retry ของ client อย่างชัดเจนว่าควรถอยกลับนานเท่าใด
ข้อดีข้อแลกเปลี่ยน
ป้องกัน overload จาก traffic spike หรือ abusive clientlegitimate user อาจถูก limit โดยไม่ตั้งใจ
protect upstream service จาก cascade loaddistributed rate limiting ซับซ้อน — ต้อง coordinate ข้าม instance
enforce fair usage ระหว่าง tenant หรือ API tierrate limit ที่ strict เกินทำให้ user experience แย่
ลด cost จาก excessive API usageต้องตั้ง limit ที่เหมาะสม — ยากถ้าไม่รู้ traffic pattern

Global Rate Limit แทน Per-client — limit รวมทั้งหมด ไม่แบ่งตาม client อาการ:

  • client หนึ่งส่ง request เยอะทำให้ client อื่นถูก limit ด้วย
  • ใช้ per-API-key หรือ per-user-ID แทน global counter

Rate Limit ที่ไม่มี Retry-After Header — reject request โดยไม่บอก client ว่ารอเท่าไร อาการ:

  • client retry ทันที ทำให้ rate limit แย่ลงกว่าเดิม
  • ส่ง Retry-After header เพื่อให้ client รู้ว่ารอนานแค่ไหน

💡 ตัวอย่างจากของจริง

GitHub API:

  • rate limit 5000 requests/hour สำหรับ authenticated user
  • 60 requests/hour สำหรับ unauthenticated
  • response header บอก remaining request และ reset time

Stripe:

  • rate limit ตาม API key
  • exponential backoff แนะนำให้ client ใช้เมื่อถูก limit
  • test mode และ live mode มี limit แยกกัน
rate limiting ปกป้องบริการจากอะไร?
token bucket อนุญาตให้เกิด burst สั้น ๆ โดยยังกำหนดขอบเขตอัตราระยะยาวได้อย่างไร?
service ควร return status code อะไรเมื่อ shed request เพราะติด rate limit
ทำไม throughput จึงพังทลายได้เมื่อ demand เกินกำลังโดยไม่มี rate limiter?