אסטרטגיית Timeout ב-API: למה 'אין הגבלת זמן' זו טעות שמפילה מערכות

מאת צוות מדיה דיל · 08.08.2026 · API Engineering · 8 דק׳

בקשה תקועה אחת בלי Timeout מפורש יכולה לצרוך את כל משאבי המערכת ולהפיל שירותים שאין להם שום קשר לתקלה המקורית. מדריך לעיצוב Timeout נכון בכל שכבה.

שרת אפליקציה נתקע לגמרי בשעת עומס שיא, למרות שאף שירות בודד לא קרס. בבדיקה מתגלה שקריאה לשירות דוחות חיצוני, שבדרך כלל עונה תוך שנייה, החלה לקחת שתי דקות בגלל עומס בצד שלו - ומכיוון שאף אחד לא הגדיר Timeout מפורש לקריאה הזו, כל בקשה שהמתינה לתשובה תפסה thread שלם ולא שחררה אותו. תוך דקות, כל ה-thread pool של השרת התמלא בבקשות תקועות, וגם בקשות שלא היה להן שום קשר לשירות הדוחות התחילו להיכשל, כי פשוט לא נותר אף thread פנוי לטפל בהן. "אין הגבלת זמן" נשמע כמו החלטה בטוחה - הרי לא רוצים לבטל בקשה שעוד עשויה להצליח - אבל בפועל זו אחת ההחלטות המסוכנות ביותר שאפשר לקבל בעיצוב API.

למה Timeout הוא לא רק "כמה זמן לחכות"

Timeout נתפס לעיתים כפרמטר טכני משני - סתם מספר שמגדירים ושוכחים ממנו. בפועל הוא החלטת עיצוב מרכזית שמשפיעה על שלוש שכבות בו-זמנית: חוויית המשתמש (כמה זמן משתמש מוכן לחכות לתשובה לפני שהוא מרגיש שהמערכת "תקועה"), ניהול משאבים (כמה זמן משאב יקר כמו thread, חיבור מסד נתונים או Connection Pool נשאר תפוס עבור בקשה בודדת), ועמידות המערכת כולה (האם בקשה תקועה אחת יכולה "להדביק" ולתקוע בקשות אחרות, כפי שראינו בתרחיש הפתיחה). בלי Timeout מפורש, המערכת מאצילה את ההחלטה הזו לברירת המחדל של ספריית הרשת שבשימוש - שלרוב היא "לחכות לנצח", וזו בדיוק ההחלטה הגרועה ביותר האפשרית.

סוגי Timeout שצריך להגדיר בנפרד

קריאת רשת בודדת מורכבת מכמה שלבים, וכל שלב צריך Timeout משלו:

  • Connection Timeout - כמה זמן לחכות לביסוס חיבור TCP ראשוני. אם השרת המרוחק לא זמין בכלל, זה צריך להיכשל מהר, בדרך כלל תוך שניות בודדות.
  • Request/Write Timeout - כמה זמן לחכות לשליחת הבקשה במלואה, רלוונטי במיוחד לבקשות עם payload גדול.
  • Response/Read Timeout - כמה זמן לחכות מרגע שליחת הבקשה ועד קבלת התחלת התשובה. זה ה-Timeout שהכי נפוץ לשכוח להגדיר, וזה בדיוק זה שגרם לתקלה בתרחיש הפתיחה, ורבים סומכים בטעות על ברירת המחדל של הספרייה בלי לבדוק שהיא אכן קיימת ולא מוגדרת כאינסופית.
  • Idle/Total Timeout - זמן כולל מקסימלי לכל הפעולה, כולל Retry אם יש. חשוב במיוחד כשיש מנגנון Retry שיכול "להאריך" בקשה בודדת הרבה מעבר למצופה.

ספריות HTTP רבות מגדירות ברירת מחדל רק לחלק מהסוגים האלה (למשל Connection Timeout בלבד), ומשאירות את השאר ללא הגבלה - חשוב לבדוק במפורש בתיעוד הספרייה בשימוש אילו Timeouts מוגדרים כברירת מחדל ואילו לא.

מדידה ורגרסיה - Timeout שדורש עדכון מתמשך

ערך Timeout שנקבע פעם אחת ונשכח הוא בעצמו סיכון. ככל שהמערכת גדלה, זמני התגובה הטבעיים של שירותים תלויים משתנים - לפעמים משתפרים בזכות אופטימיזציה, ולפעמים מחמירים בזכות עומס גובר. לכן כדאי לנטר את יחס הבקשות שמגיעות בפועל לגבול ה-Timeout (ולא רק להסתכל על שגיאות מוחלטות), ולהתייחס לעלייה במגמה הזו כאינדיקציה מוקדמת לבעיה, בדיוק כמו שמתייחסים לעלייה בשיעור השגיאות. חלק מהצוותים הבוגרים יותר אף מממשים Timeout אדפטיבי - ערך שמתעדכן אוטומטית על בסיס מדידות P99 מתגלגלות, ולא נשאר קבוע לנצח בקוד.

קביעת ערכים - לא ניחוש, אלא מדידה של P99

הטעות הנפוצה בקביעת ערך Timeout היא ניחוש עגול (5 שניות? 30 שניות?) בלי בסיס נתונים. הגישה הנכונה היא למדוד את זמן התגובה בפועל של השירות תחת עומס רגיל, ולהגדיר Timeout מעט מעל אחוזון גבוה (P99 או P99.9) - כלומר, הזמן שבו 99 אחוז מהבקשות הרגילות כבר הסתיימו. Timeout קרוב מדי לזמן החציוני (P50) יגרום לבטל בקשות איטיות אך לגיטימיות; Timeout רחוק מדי מה-P99 יגרום לבקשות תקועות באמת להמתין הרבה יותר זמן ממה שנחוץ לפני שהמערכת מגיבה. חשוב גם לעדכן את הערך תקופתית - אם השירות משתפר ומהיר יותר עם הזמן, Timeout ישן ורחוק מדי משאיר "רשת ביטחון" גדולה מדי ומיותרת שעדיין חושפת אתכם לתקיעות ארוכות.

Timeout שרשרתי - כשקריאה אחת קוראת לעוד קריאה

במערכות מבוזרות, בקשה אחת מהמשתמש מפעילה לעיתים שרשרת שלמה של קריאות פנימיות - שירות A קורא לשירות B שקורא לשירות C. אם לכל שכבה יש Timeout עצמאי שלא מתחשב בזמן שכבר חלף, אפשר להגיע למצב אבסורדי שבו הלקוח הראשוני כבר ויתר וקיבל שגיאת Timeout, אבל שירות C עדיין ממשיך לעבוד על אותה בקשה בדיוק, כי אף אחד לא הודיע לו שהתשובה כבר לא נחוצה. הפתרון הנכון הוא Timeout Budget - זמן כולל שמוקצה לבקשה מרגע כניסתה למערכת, שמועבר ומצטמצם עם כל קפיצה פנימית (למשל דרך כותרת HTTP ייעודית), כך שכל שכבה יודעת בדיוק כמה זמן נותר לה, ויכולה לבטל את הפעולה מוקדם אם התקציב אזל.

// כותרת שמעבירה את התקציב שנותר, מצטמצם בכל קפיצה
X-Request-Deadline: 1699999999500  // Unix ms

function remainingBudget(req) {
  const deadline = parseInt(req.headers['x-request-deadline']);
  return Math.max(0, deadline - Date.now());
}

ביטול פעיל - לא רק לחכות שהזמן ייגמר

הגדרת Timeout שקובעת מתי להפסיק לחכות לתשובה היא רק חצי מהפתרון. החצי השני, שקל לפספס, הוא ביטול פעיל (Cancellation) של הפעולה עצמה בצד השני. אם קריאת HTTP פגה בזמן צד הלקוח, אבל השרת המרוחק ממשיך לעבד את הבקשה בכל מקרה (כי אין לו דרך לדעת שהלקוח כבר ויתר), המשאבים בצד השרת עדיין מתבזבזים על עבודה שהתוצאה שלה כבר לא רלוונטית לאף אחד ולעולם לא תישלח לשום מקום. תמיכה מלאה ונכונה ב-Cancellation - למשל דרך AbortController ב-JavaScript, או context.Context ב-Go - מאפשרת להפסיק בפועל את העבודה בצד השרת ברגע שהלקוח כבר לא מעוניין בתשובה, ולא רק "לוותר על ההמתנה" בצד הלקוח בזמן שהעבודה ממשיכה לרוץ ללא צורך.

Timeout ו-Connection Pool - הקשר שקל לפספס

Timeout ארוך מדי לא רק מסכן בקשה בודדת - הוא משפיע ישירות על גודל ה-Connection Pool הנדרש למערכת כולה. אם כל חיבור למסד נתונים או לשירות חיצוני יכול להישאר תפוס עד 60 שניות, ובעומס שיא מגיעות מאה בקשות בו-זמנית, נדרש Pool של מאה חיבורים כדי לא להיתקע - וזה לפני שמביאים בחשבון תרחישי כשל שבהם הרבה בקשות איטיות מצטברות יחד. Timeout הדוק יותר מקטין את זמן ההחזקה הממוצע של כל חיבור, ומאפשר ל-Pool קטן יותר לשרת נפח בקשות גבוה יותר. חישוב גודל ה-Pool הנדרש צריך להתחשב מפורשות בערך ה-Timeout שנבחר, לא רק בכמות הבקשות הצפויה - שני הפרמטרים תלויים זה בזה ישירות. בפועל, צוותים רבים מגלים את הקשר הזה רק בדיעבד - הם מגדילים את ה-Pool כדי לפתור תסמין של "אין חיבורים פנויים", בלי לשים לב שהבעיה האמיתית היא Timeout רחב מדי שמחזיק חיבורים הרבה יותר זמן מהנדרש, ושהגדלת ה-Pool רק דוחה את אותה בעיה לעומס הבא.

Streaming ו-Long-polling - כשה-Timeout הרגיל לא רלוונטי

לא כל קריאה מתאימה למודל של Timeout קבוע. חיבורי Streaming (כמו Server-Sent Events או WebSocket) וקריאות Long-polling נועדו במפורש להישאר פתוחים לפרקי זמן ארוכים, ולכן דורשים אסטרטגיה שונה - לא Timeout שמבטל את החיבור, אלא מנגנון Heartbeat או Keep-alive שמוודא שהחיבור עדיין חי ותקין, ומבטל אותו רק כשה-Heartbeat עצמו נכשל. ערבוב בין שני המודלים - הטמעת Timeout רגיל על חיבור Streaming - יגרום לניתוקים תכופים ומיותרים בדיוק במקום שבו רציפות החיבור היא הדרישה המרכזית.

Trade-offs - Timeout קצר מדי מול ארוך מדי

Timeout קצר מדי גורם לבקשות איטיות אך לגיטימיות (למשל שאילתת דוח כבד שלוקח באמת יותר זמן) להיכשל בטעות, מה שיוצר חוויית משתמש גרועה ותסכול מיותר. Timeout ארוך מדי חוזר לבעיה המקורית - מאפשר לבקשה תקועה לצרוך משאבים לזמן ארוך ולסכן את יציבות המערכת כולה. הפתרון האמיתי לא תמיד הוא "למצוא את המספר המושלם" - לעיתים קרובות יותר נכון להבדיל בין endpoints לפי אופי הפעולה: endpoint שמחזיר תשובה מהירה (כמו שליפת רשומה בודדת) צריך Timeout הדוק מאוד (שניה-שתיים), בעוד endpoint שידוע כבעל זמן עיבוד ארוך (כמו יצירת דוח מורכב) עדיף שיהפוך לפעולה א-סינכרונית עם Polling או Webhook, במקום לנסות "לתפוס" את התוצאה בקריאה סינכרונית ארוכה עם Timeout גבוה שמסכן את ה-thread שמחזיק אותה - החלטה ארכיטקטונית שנראית קטנה אבל חוסכת בעיות יציבות משמעותיות בהמשך הדרך.

טעויות נפוצות בפרודקשן

הטעות הראשונה, כמובן, היא חוסר הגדרה מפורשת של Timeout כלל, כפי שראינו בתרחיש הפתיחה. השנייה היא הגדרת Timeout רק בשכבה אחת (למשל בקוד האפליקציה) בלי לוודא שהוא עקבי עם שכבות אחרות (Load Balancer, Reverse Proxy) - אם ה-Proxy חותך חיבור אחרי 60 שניות אבל האפליקציה מוגדרת ל-90 שניות, הלקוח מקבל שגיאת חיבור סתומה במקום תשובה ברורה. השלישית היא Timeout זהה לכל סוגי הקריאות בלי אבחנה בין endpoint מהיר לאיטי, מה שגורם לפשרה גרועה - קצר מדי לאחד וארוך מדי לשני. הרביעית, המסוכנת ביותר, היא Timeout בלי ביטול פעיל - שכפי שראינו משאיר עבודה מתבזבזת בצד השרת גם אחרי שהלקוח כבר ויתר.

מתי כן להגדיר Timeout קפדני ומתי אפשר להרפות

Timeout קפדני ומדוד היטב הוא קריטי לכל קריאה סינכרונית שמשתמש בזמן אמת ממתין לה, ולכל קריאה בין שירותים במערכת מבוזרת. הוא פחות קריטי (אבל עדיין רצוי) לתהליכי רקע א-סינכרוניים שאין להם לחץ זמן משתמש - שם אפשר להרשות Timeout ארוך יותר, כל עוד יש מנגנון נפרד שמזהה תהליכים "תקועים" ומטפל בהם (למשל דרך תור עם Dead Letter Queue). קו מנחה פשוט: אם בקשה תקועה יכולה לצרוך משאב משותף (thread, חיבור מסד נתונים), Timeout הדוק הוא לא אופציונלי; אם היא רצה מבודדת בתהליך רקע ייעודי, יש יותר מרחב תמרון.

סיכום

"אין הגבלת זמן" היא לא ברירת מחדל בטוחה - היא הימור מסוכן שהופך תקלה נקודתית בשירות אחד לקריסה של המערכת כולה. עיצוב נכון של Timeout דורש הבחנה בין סוגי ההשהיה השונים, מדידה מבוססת P99 ולא ניחוש, תקציב זמן שעובר בין שכבות מבוזרות, וביטול פעיל שמפסיק עבודה מיותרת בפועל, לא רק המתנה שנפסקת. ההשקעה הזו קטנה יחסית לנזק שהיא מונעת - וברוב המקרים שאנחנו רואים במדיה דיל, בדיוק הפרט הקטן הזה הוא זה שמכריע האם תקלה נקודתית נשארת מקומית או הופכת לקריסה רוחבית של כל המערכת. שילוב Timeout מדוד היטב עם Retry אחראי ו-Circuit Breaker בשכבה שמעליו הוא מה שהופך מערכת מבוזרת מאוסף שברירי של תלויות למערכת שבאמת שורדת תקלות חלקיות בלי לגרור את כולן יחד.

תגיות: Timeout · P99 Latency · Connection Pool · Cancellation · Microservices · API Reliability · Thread Pool

← חזרה לבלוג · צור קשר