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

การจำกัดอัตราคำขอ

rate limiter คือกลไกที่จำกัดจำนวนคำขอที่ไคลเอนต์สามารถส่งได้ภายในช่วงเวลาที่กำหนด Redis ทำให้สิ่งนี้ทำได้ง่ายด้วยคำสั่ง atomic สองตัว ได้แก่ INCR สำหรับนับคำขอ และ EXPIRE สำหรับกำหนดขอบเขตของช่วงเวลา

สำหรับคำขอที่เข้ามาแต่ละครั้ง ให้เพิ่มค่า counter ที่มี key ระบุตามผู้ใช้และช่วงเวลา เมื่อมีการเรียกครั้งแรก ซึ่งก็คือเมื่อ counter มีค่าเท่ากับ 1 ให้ตั้งค่า expiry บน key นั้น หาก counter เกินขีดจำกัด ให้ปฏิเสธคำขอนั้น

flowchart TD
  A["Incoming request"] --> B["count = INCR rate:${userId}:window"]
  B --> C{"count == 1?"}
  C -->|"Yes (first hit)"| D["EXPIRE rate:${userId}:window 60"]
  C -->|"No"| E{"count > 100?"}
  D --> E
  E -->|"Yes"| F["Reject (HTTP 429)"]
  E -->|"No"| G["Allow request"]
Fixed-window rate limiter: INCR the counter, set EXPIRE on the first hit, reject once the limit is exceeded

key ที่เข้ารหัสช่วงเวลาไว้ ทำให้แต่ละช่วงเวลาใหม่ได้รับ counter ใหม่โดยอัตโนมัติ

127.0.0.1:6379> INCR rate:user:42:window1
(integer) 1
127.0.0.1:6379> EXPIRE rate:user:42:window1 60
(integer) 1
127.0.0.1:6379> INCR rate:user:42:window1
(integer) 2
127.0.0.1:6379> TTL rate:user:42:window1
(integer) 58

INCR ครั้งแรกคืนค่า 1 จึงตั้งค่า expiry ทันที การเพิ่มค่าครั้งถัดไปจะนับในช่วงเวลาเดิม และ TTL ยืนยันว่า key จะหมดอายุหลังจากช่วงเวลาสิ้นสุด

INCR rate:user:42:window1
EXPIRE rate:user:42:window1 60
INCR rate:user:42:window1
TTL rate:user:42:window1

INCR + EXPIRE ให้ fixed window: คำขอทั้งหมดใน bucket 60 วินาทีเดียวกันจะใช้ counter ร่วมกัน การส่งคำขอจำนวนมากที่ปลายของช่วงเวลาหนึ่งและต้นของช่วงเวลาถัดไปอาจทำให้อัตราที่อนุญาตเพิ่มขึ้นเป็นสองเท่าที่ขอบเขตได้

สำหรับ sliding window ที่แท้จริง คุณจะต้องใช้ sorted set: เก็บ timestamp ของแต่ละคำขอด้วย ZADD ตัดรายการเก่าออกด้วย ZREMRANGEBYSCORE แล้วนับรายการที่ยังมีผลด้วย ZCARD วิธีนี้แม่นยำกว่าแต่มีต้นทุนต่อคำขอสูงกว่าเล็กน้อย

ตัวเลือกBenefitCost
Fixed window (INCR + EXPIRE)ง่าย เร็ว ใช้เพียง 2 คำสั่งต่อ requestอัตราที่อนุญาตอาจเพิ่มเป็นสองเท่าได้ที่รอยต่อของ window
Sliding window (ZADD + ZREMRANGEBYSCORE)แม่นยำกว่า ไม่มี edge burstใช้ memory และ CPU มากกว่าต่อ request หนึ่งครั้ง
Token bucketรองรับ burst traffic แบบควบคุมได้ต้องคำนวณ refill rate เพิ่มความซับซ้อนของ logic
  • ลืมตั้ง EXPIRE ตอน count == 1 — ถ้าไม่ตั้ง TTL ให้ counter key จะไม่มีวันรีเซ็ต ผู้ใช้จะโดน rate limit ถาวรหลังแตะขีดจำกัดครั้งแรก
  • ใช้ fixed window แล้วคิดว่าป้องกัน burst ได้ 100% — คำขอที่กระจุกอยู่ปลาย window หนึ่งกับต้น window ถัดไปสามารถรวมกันเกินขีดจำกัดจริงได้ถึงสองเท่า
  • ไม่คืน HTTP header ที่บอกสถานะ rate limit ให้ client — ถ้าไม่ส่ง header อย่าง Retry-After client จะ retry แบบสุ่มแทนที่จะรอเวลาที่เหมาะสม

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

GitHub API — ใช้ counter แบบ INCR/EXPIRE ต่อ token เพื่อจำกัดจำนวน request ต่อชั่วโมง พร้อมส่ง header บอกโควตาที่เหลือ

Cloudflare — ใช้กลไก rate limiting ที่อิงหลักการเดียวกับ sliding window เพื่อป้องกัน DDoS และ abuse ที่ edge ก่อนถึง origin server

ทำไมเราจึงเรียก EXPIRE เฉพาะเมื่อ count == 1?
HTTP status code ใดที่ควรส่งคืนเมื่อถูก rate limit?
INCR เป็น atomic หมายความว่าอย่างไรสำหรับคำขอที่เกิดขึ้นพร้อมกัน?
โครงสร้างข้อมูลใดรองรับ sliding-window rate limiter ที่แท้จริง?