Rate Limiting ו-Backoff ל-LLM בייצור ב-2026: התמודדות עם 429, TPM ו-RPM

מדריך מעשי לניהול rate limits של LLM בייצור ב-2026: הבנת TPM ו-RPM, exponential backoff עם jitter, circuit breaker, Redis token bucket מבוזר ותור עדיפויות ל-multi-tenant SaaS. עם דוגמאות קוד Python מלאות.

Rate Limiting ל-LLM: מדריך 2026

עודכן: 3 בספטמבר 2026

Rate limiting ל-LLM בייצור הוא הפרקטיקה של ניהול קצב הבקשות (RPM) והטוקנים (TPM) שהאפליקציה שלכם שולחת לספקי מודלים כמו OpenAI, Anthropic ו-Google, כדי למנוע שגיאות 429, להבטיח לטנציה יציבה ולשלוט בעלויות. בשנת 2026, כל ספק מודל גדול אוכף מכסות נפרדות לבקשות ולטוקנים, ואפליקציה בייצור חייבת לשלב exponential backoff, circuit breaker ותור מבוזר כדי לא לקרוס תחת עומס. במאמר הזה אני משתפת את הארכיטקטורה שבניתי אחרי שלוש תקריות פרודקשן שהיו יכולות להימנע (כן, גם את הראשונה).

  • ספקי LLM אוכפים שתי מגבלות במקביל: RPM (Requests Per Minute) ו-TPM (Tokens Per Minute). חריגה בכל אחד מהם מחזירה HTTP 429.
  • Exponential backoff עם jitter הוא ברירת המחדל הנכונה, אך צריך להוסיף לו תקרת retry (max 5) ולכבד את Retry-After אם הספק מספק אותו.
  • Circuit Breaker נדרש כאשר ההיסטוריה מראה כשלים רצופים, כי הוא חוסך שרשור של timeouts ומגן על ה-thread pool.
  • ל-multi-tenant SaaS, הגבלת קצב אפליקטיבית חייבת להיות מבוזרת (Redis Token Bucket) ולא רק מקומית.
  • לפני שאתם מגיבים לתקרית 429, ודאו שהדשבורד שלכם מציג TPM/RPM נוכחיים לעומת המכסה. 80% מהתקריות שראיתי נגרמו מ-tier נמוך מדי, לא מבאג.

מה זה Rate Limiting ב-LLM וכיצד הוא עובד

Rate limiting ב-LLM הוא מנגנון שספקי המודל משתמשים בו כדי להגן על התשתית שלהם ולהבטיח שימוש הוגן בין לקוחות. בניגוד ל-REST APIs קלאסיים שסופרים רק בקשות, ספקי LLM אוכפים לפחות שתי מגבלות מקבילות: RPM (Requests Per Minute), כלומר כמה קריאות API בדקה, ו-TPM (Tokens Per Minute), כמה טוקנים סך הכל (קלט + פלט) נצרכו בחלון של דקה. חלקם מוסיפים גם מגבלות יומיות (RPD, TPD) ומגבלות במקביל (concurrent requests).

המשמעות המעשית: אפשר לצרוך את כל מכסת ה-TPM שלכם עם 3 בקשות ארוכות ולקבל 429 גם כשה-RPM רחוק מהתקרה. אני זוכרת תקרית שבה מיגרנו prompt template אחד מ-2K טוקנים ל-12K טוקנים בגלל שהוספנו few-shot examples. ה-TPM שלנו קרס אחרי 4 שעות, והצוות לא הבין למה. הלקח: תמיד למדוד גם TPM, לא רק RPM.

ספקי LLM גם מיישמים Tier system. ככל שאתם משלמים יותר או צוברים היסטוריה, המכסה עולה אוטומטית. ב-OpenAI, מעבר מ-Tier 3 ל-Tier 4 מכפיל את ה-TPM פי חמש ל-gpt-4o. כדאי לבדוק את ה-tier שלכם לפני שאתם משקיעים שבועות באופטימיזציה של קוד, כי לפעמים העלאת ה-tier דרך תשלום מקדים פותרת את הבעיה תוך יום.

השוואת מגבלות בין OpenAI, Anthropic, Google ו-Cohere

בדקתי בספטמבר 2026 את המכסות של ארבעה ספקי LLM הפופולריים ביותר בייצור. הטבלה הבאה מסכמת את מה שחשוב לדעת לפני בחירת ספק, כי שדרוג tier לוקח זמן ולא תמיד אוטומטי.

מאפייןOpenAIAnthropicGoogle GeminiCohere
מגבלת RPM (Tier 1)500 (gpt-4o)50 (Claude Sonnet)360 (Gemini 2.0)10,000
מגבלת TPM (Tier 1)30K40K4M10M
מספר Tiers542 (Free/Paid)2 (Trial/Prod)
Header לזמן המתנהRetry-Afterretry-afterאיןRetry-After
מגבלות חד-בקשהBatch API עוקףPrompt Caching מפחית TPMContext של 2M טוקניםCompressed prompts
Priority AccessScale Tier ($$$)Priority TierEnterpriseReserved capacity

שני דגשים חשובים: ראשית, המספרים משתנים לפי מודל ספציפי (gpt-4o-mini יקבל TPM גבוה משמעותית מ-o1). שנית, Google לא מספק header של Retry-After, כך שאתם צריכים לחשב את זמן ההמתנה בעצמכם באמצעות backoff אקספוננציאלי טהור. תוכלו לקרוא את ה-תיעוד הרשמי של OpenAI על rate limits ואת ה-מדיניות המכסות של Anthropic כדי לוודא את המספרים העדכניים לחשבון שלכם.

איך להתמודד עם שגיאת 429 בצורה נכונה

שגיאת HTTP 429 (Too Many Requests) היא האותת של הספק שאתם צריכים להאט. הטעות הנפוצה שאני רואה בקוד לקוחות היא retry מיידי בלולאה. זה בדרך כלל גורם ל-thundering herd, כי כל הבקשות שנכשלו מנסות שוב באותו רגע ומקבלות שוב 429. הפתרון הנכון מורכב מארבעה שלבים.

שלב 1, כיבוד ה-Retry-After: אם הספק החזיר את ה-header הזה (OpenAI ו-Anthropic כן, Google לא), השתמשו בו ישירות ולא בחישוב שלכם. הוא מייצג את הזמן המדויק שבו המכסה שלכם תתחדש.

שלב 2, הוספת Jitter: גם אחרי המתנה של Retry-After, הוסיפו רעש אקראי של 0-500ms. אם אפליקציה שלכם רצה על 10 מכונות, כולן קיבלו את אותה תשובת 429 באותה שנייה, ובלי jitter כולן ינסו שוב באותו רגע.

שלב 3, תקרת retries: אף פעם אל תקבעו retry אינסופי. תקרה של 5 ניסיונות היא סטנדרט תעשייתי; אחרי זה עדיף להחזיר שגיאה למשתמש מאשר להחזיק חיבור פתוח לנצח.

שלב 4, נפילה למודל חלופי: אם GPT-4o במגבלה, נסו את Claude Sonnet או Gemini 2.5. זה בדיוק המקום שבו שערי LLM כמו LiteLLM ו-Portkey מוכיחים את הערך שלהם, כי הם מנהלים את ה-failover אוטומטית ברמת התשתית.

Exponential Backoff עם Jitter: קוד מלא ב-Python

Exponential backoff הוא האלגוריתם הסטנדרטי להתמודדות עם 429 בבטחה. הרעיון פשוט: אחרי כשלון N, המתן base * 2^N שניות (בתוספת jitter) לפני הניסיון הבא. הספרייה Tenacity היא הבחירה שלי בייצור כי היא תומכת ב-async, מבוססת decorators, ומאפשרת שילוב של תנאים מותאמים.

import asyncio
import random
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential_jitter,
    retry_if_exception_type,
    before_sleep_log,
)
import logging
from openai import AsyncOpenAI, RateLimitError, APITimeoutError

logger = logging.getLogger(__name__)
client = AsyncOpenAI()

@retry(
    retry=retry_if_exception_type((RateLimitError, APITimeoutError)),
    wait=wait_exponential_jitter(initial=1, max=60, jitter=2),
    stop=stop_after_attempt(5),
    before_sleep=before_sleep_log(logger, logging.WARNING),
    reraise=True,
)
async def call_llm_with_backoff(prompt: str) -> str:
    response = await client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        timeout=30.0,
    )
    return response.choices[0].message.content

# שימוש בייצור
async def main():
    try:
        result = await call_llm_with_backoff("סכם את המסמך הבא...")
        print(result)
    except RateLimitError as e:
        # אחרי 5 retries נכשלנו, יש להחזיר תשובה חלופית
        logger.error(f"נכשל אחרי 5 ניסיונות: {e}")
        return {"error": "השירות לא זמין כרגע, נסה שוב בעוד דקה"}

שימו לב לפרטים חשובים: wait_exponential_jitter מוסיף רעש אקראי אוטומטית, stop_after_attempt(5) קובע תקרה של 5 ניסיונות, ו-timeout=30.0 על ה-API call עצמו הכרחי. בלי טיימאאוט, הבקשה יכולה להיתקע לדקות ולחסום את הפול. במקרה של הצלחה: הפונקציה מחזירה את התשובה בקריאה הראשונה בלי overhead.

לקוראים שרוצים להיכנס לעומק תכנון prompts יעילים שיפחיתו את צריכת ה-TPM מלכתחילה, מומלץ לעיין במדריך המלא ל-Prompt Caching ואופטימיזציית עלויות LLM.

מתי להשתמש ב-Circuit Breaker במקום ב-Retry

Circuit Breaker הוא תבנית עיצוב שמנטרת שיעור כשלונות ומפסיקה זמנית לשלוח בקשות כשהספק "חולה". Retry רגיל מניח שהכשלון הוא זמני ומקומי (בקשה ספציפית); Circuit Breaker מזהה שהבעיה שיטתית (הספק במלוא, אירוע incident, DNS פגום) ומגן על כל שאר המערכת ממנה. בייצור, שני הדפוסים משלימים זה את זה ולא מחליפים.

Circuit Breaker עובד בשלושה מצבים: Closed (רגיל, כל הבקשות עוברות), Open (חסום, כל בקשה נופלת מיד בלי לפגוע בספק), ו-Half-Open (מנסה שוב אחרי cooldown, אם מצליחה חוזרים ל-Closed). ההפרדה חשובה כי היא מונעת cascade failure. בלי circuit breaker, 1000 בקשות ליחידת זמן ימשיכו להיכנס לתור, יגיעו לטיימאאוט ויסתמו את ה-thread pool.

from pybreaker import CircuitBreaker, CircuitBreakerListener
import logging

logger = logging.getLogger(__name__)

class LLMBreakerListener(CircuitBreakerListener):
    def state_change(self, cb, old_state, new_state):
        logger.warning(
            f"Circuit breaker {cb.name}: {old_state.name} -> {new_state.name}"
        )

llm_breaker = CircuitBreaker(
    fail_max=10,        # אחרי 10 כשלונות
    reset_timeout=60,   # ננסה שוב אחרי 60 שניות
    exclude=[ValueError],  # אל תספור שגיאות ולידציה
    listeners=[LLMBreakerListener()],
    name="openai-gpt-4o",
)

@llm_breaker
async def protected_llm_call(prompt: str) -> str:
    return await call_llm_with_backoff(prompt)

# בנקודת קריאה: תפוס CircuitBreakerError בנפרד
try:
    result = await protected_llm_call("...")
except pybreaker.CircuitBreakerError:
    # ה-Breaker פתוח, השתמש בחלופה
    result = await fallback_to_claude("...")

שילוב זה עם ה-backoff מהסעיף הקודם נותן שכבת הגנה כפולה: retries מטפלים בכשלונות transient, circuit breaker מגן מקריסה שיטתית. אני ממליצה בחום להשתמש ב-pybreaker ל-Python או opossum ל-Node.js. שתיהן נבדקו בייצור בקנה מידה גדול.

Rate Limiting מבוזר עם Redis Token Bucket

אם האפליקציה שלכם רצה על יותר ממכונה אחת (וב-2026 זה כמעט כל שירות בייצור), rate limiting מקומי לא מספיק. כל instance יידע רק על הבקשות שלו, ובסך הכל תעברו את המכסה של הספק. הפתרון הוא Token Bucket מבוזר ב-Redis: כל השרתים חולקים "דלי" משותף שמתמלא בקצב קבוע.

אלגוריתם Token Bucket עובד כך: בכל דקה, הדלי מקבל N טוקנים חדשים (עד תקרה). לפני שליחת בקשה ל-LLM, האפליקציה שואבת טוקנים מהדלי בגודל של הבקשה (input + output estimate). אם אין מספיק, הבקשה ממתינה או נדחית. הנה מימוש עם Lua script אטומי:

import redis.asyncio as redis
import time
from typing import Optional

# Lua script — פועל אטומית ב-Redis
TOKEN_BUCKET_SCRIPT = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])

local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now

-- מלא את הדלי לפי הזמן שעבר
local elapsed = now - last_refill
tokens = math.min(capacity, tokens + elapsed * refill_rate)

if tokens >= requested then
    tokens = tokens - requested
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
    redis.call('EXPIRE', key, 3600)
    return 1
else
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
    redis.call('EXPIRE', key, 3600)
    return 0
end
"""

class DistributedRateLimiter:
    def __init__(self, redis_url: str, tpm: int):
        self.redis = redis.from_url(redis_url)
        self.capacity = tpm
        self.refill_rate = tpm / 60.0  # טוקנים לשנייה

    async def try_acquire(self, tokens_needed: int, key: str) -> bool:
        script = self.redis.register_script(TOKEN_BUCKET_SCRIPT)
        result = await script(
            keys=[f"llm_bucket:{key}"],
            args=[self.capacity, self.refill_rate, tokens_needed, time.time()],
        )
        return bool(result)

# שימוש
limiter = DistributedRateLimiter("redis://localhost:6379", tpm=30_000)
estimated_tokens = len(prompt) // 4 + 500  # קירוב + פלט צפוי

if await limiter.try_acquire(estimated_tokens, key="openai-gpt-4o"):
    result = await call_llm_with_backoff(prompt)
else:
    return {"error": "מכסת TPM נגמרה, נסה שוב בעוד רגע"}

ה-Lua script הזה קריטי: הוא מבטיח שהבדיקה, העדכון וההחלטה קורים אטומית ב-Redis בלי race conditions. בלי זה, שני instances יכולים לקרוא את אותו מצב במקביל ולעבור את המכסה. בקנה מידה של 50+ מכונות, זה קורה כל דקה. אני נכוויתי מזה פעם, בסופ"ש, במהלך קמפיין שיווקי.

תור עדיפויות ל-multi-tenant SaaS

ב-SaaS מבוסס LLM עם רבדי תמחור (Free/Pro/Enterprise), חייבים להעדיף בקשות של לקוחות משלמים כאשר מגיעים למגבלת TPM. Rate limiting פשוט מתייחס לכל הבקשות שווה, וזאת בעיה כאשר קמפיין חינמי צורך את כל המכסה ולקוחות Enterprise מקבלים 429.

הפתרון: Weighted Fair Queuing עם משקלים לפי tier. Redis Streams מספקים את התשתית הנכונה: כל בקשה נכנסת לתור עם priority score, ו-worker שולף את הבקשה בעלת ה-priority הגבוה ביותר שיש לה מספיק tokens.

from enum import IntEnum
import asyncio
import json

class Priority(IntEnum):
    FREE = 1
    PRO = 5
    ENTERPRISE = 10

async def enqueue_llm_request(user_tier: Priority, prompt: str, tokens: int):
    request = {
        "prompt": prompt,
        "tokens": tokens,
        "priority": user_tier.value,
        "enqueued_at": time.time(),
    }
    # priority ב-Redis Sorted Set: score גבוה = יוצא ראשון
    await redis_client.zadd(
        "llm_queue",
        {json.dumps(request): user_tier.value * 1000 + time.time()},
    )

async def worker():
    while True:
        # שלוף את הבקשה בעלת ה-priority הגבוה ביותר
        items = await redis_client.zpopmax("llm_queue", count=1)
        if not items:
            await asyncio.sleep(0.1)
            continue

        request_json, _ = items[0]
        request = json.loads(request_json)

        # בקש טוקנים מהדלי המבוזר
        if await limiter.try_acquire(request["tokens"], "openai-gpt-4o"):
            asyncio.create_task(process_llm(request))
        else:
            # אין טוקנים, החזר לתור עם delay
            await asyncio.sleep(1)
            await redis_client.zadd(
                "llm_queue",
                {request_json: request["priority"] * 1000 + time.time()},
            )

שתי הערות חשובות ליישום: ראשית, ה-priority * 1000 + time.time() מבטיח שבתוך אותה עדיפות, בקשות ישנות יותר יוצאות ראשונות (FIFO בתוך tier). שנית, כדאי לעקוב אחרי זמן ההמתנה של Free users כדי לא לתת חוויה גרועה מדי. אם חורגים מ-30 שניות, אולי הגיע הזמן לשדרג את ה-tier של הספק. לניהול נכון של רבדים, כדאי לשלב עם המדריך להנדסת הקשר לבניית מערכות AI לייצור כדי לבחור אורך prompt מותאם ל-tier.

תצפית ומטריקות לניהול Rate Limits

בלי דשבורד, לא תדעו שהגעתם ל-80% מהמכסה עד שאתם מקבלים 429. הנה חמש המטריקות שאני חייבת בדשבורד לכל שירות LLM בייצור, לרוב ב-Grafana מעל Prometheus, אבל Langfuse ו-LangSmith לתצפית על LLM מכסים את רובן אוטומטית:

  • TPM נוכחי / TPM מקסימלי (per model, per tier). זה הגרף החשוב ביותר, עם Alert על 80%.
  • שיעור 429 בחלון של 5 דקות. אם עולה מעל 1%, יש בעיה.
  • זמן backoff מצטבר לכל בקשה מוצלחת. מדד ללחץ על המערכת, וגידול פירושו שהמכסה לא מספיקה.
  • מצב Circuit Breaker per provider (Open/Closed/Half-Open), כמה זמן שהה בכל מצב.
  • עומק תור priority per tier. אם Free queue גדל אקספוננציאלית, זה סימן שאתם צריכים לשדרג tier או להוסיף provider חלופי.

בנוסף לאלה, חשוב לעקוב אחרי p50/p95/p99 של זמני תגובה, כי backoff מוסיף לטנציה שקטה שלא רואים ב-error rate. תקרית של הצוות שלי לפני חצי שנה: ה-latency p99 קפצה מ-2s ל-14s, אבל error rate נשאר 0%. התברר שהיינו במצב של retries רצופים בגלל TPM מלא. הדשבורד גילה את זה תוך 10 דקות.

שאלות נפוצות

מה ההבדל בין TPM ל-RPM ב-API של LLM?

RPM (Requests Per Minute) סופר כמה קריאות API בדקה, ו-TPM (Tokens Per Minute) סופר את סך הטוקנים (קלט + פלט) שנצרכו. שתי המגבלות פועלות במקביל, וחריגה מכל אחת מהן מחזירה HTTP 429. בפועל, TPM הוא לרוב הצוואר בקבוק בזרימות ארוכות (סיכומים, RAG), בעוד RPM הוא הבעיה בממשקי chat קצרים בקנה מידה גדול.

איך להתמודד עם שגיאת 429 מ-OpenAI בצורה נכונה?

הפעילו exponential backoff עם jitter, כבדו את ה-header Retry-After אם קיים, קבעו תקרה של 5 ניסיונות, ואם כל הניסיונות נכשלים, עברו למודל חלופי (fallback) או החזירו הודעת שגיאה מתאימה למשתמש. הימנעו מ-retry אינסופי או retry ללא jitter, שניהם גורמים ל-thundering herd.

מתי כדאי להשתמש ב-Circuit Breaker במקום ב-retry רגיל?

Retry מטפל בכשלונות transient בבקשה בודדת. Circuit Breaker נדרש כאשר הבעיה שיטתית (ספק במלוא, incident, DNS פגום) כדי למנוע cascade failure. בייצור, השתמשו בשניהם: retry בשכבה הפנימית, circuit breaker בשכבה החיצונית שמגן על ה-thread pool.

האם צריך Redis ל-rate limiting אם יש רק שרת אחד?

לא. עם שרת יחיד, מונה in-memory עם asyncio.Semaphore או ספרייה כמו aiolimiter מספיק. Redis נדרש רק כאשר יש 2+ instances שחולקים את אותה מכסה של הספק, אחרת כל instance יאמין שיש לו את כל המכסה ותעברו אותה בסך הכל.

איך להעריך כמה טוקנים בקשה תצרוך לפני שליחתה?

לקלט: השתמשו בספרייה tiktoken (OpenAI) או anthropic-tokenizer, והן מחזירות ספירה מדויקת. לפלט: הערכה שמרנית של max_tokens שהגדרתם, או ממוצע נצפה מהיסטוריה. תמיד חייבו את שני הצדדים בבקשת ה-token bucket, אחרת תעברו את המכסה כשהמודל יחזיר פלט ארוך יותר מהצפוי.

Cara Donovan
אודות הכותב Cara Donovan

AI operations lead at a B2B SaaS. Builds the unglamorous infrastructure that keeps prod LLM apps from melting.