- טבלת החלטה KV / D1 / R2 / Durable Objects על 6 עמודות: consistency, פרופיל latency, מחיר/מגבלה חינמית, המגבלה שתיפול ראשונה, ו-use-case מובהק — עם 3 תרחישים משויכים ומנומקים.
- קובץ
wrangler.tomlאחד עם 4 bindings פעילים (KV + D1 + R2 + Durable Object עםnew_sqlite_classes), מורחב מאותו קובץ שבנית בפרק 1. - Worker עם 4 נתיבים — כל אחד קורא/כותב ל-primitive שלו (KV put/get, D1 SELECT עם WHERE, R2 put/get, DO counter ב-SQLite), מאומת ב-
wrangler devוב---remote. - מחשבון מגבלות: מקבל N משתמשים ופעולות-ליום, ומסמן איזו מגבלה חינמית תיפול ראשונה ומתי בדיוק $5 הופך מוצדק.
- מודל מנטלי חד של קיר ה-10ms: לזהות מבט אחד אילו שורות שורפות CPU (parse/regex/לולאות) ואילו לא (await על fetch/KV/D1).
- הבנה למה ב-free plan אתה לא מקבל חשבון הפתעה — אתה פוגע בקיר ופעולות נכשלות עד reset ב-00:00 UTC.
- להבחין בין CPU time ל-wall-clock time ולזהות אילו פעולות שורפות את 10ms (parse/regex/לולאות) ואילו לא (
awaitעלfetch/KV/D1). - לבחור בין KV, D1, R2 ו-Durable Objects בהינתן תרחיש, ולנמק לפי consistency, מחיר ומגבלה.
- לחבר bindings ב-
wrangler.toml(KV, D1, R2, DO) ולקרוא/לכתוב מתוך Worker. - לחשב את המגבלה שתיפול ראשונה בכל primitive (KV 1k writes/day, D1 100k writes/day, DO 13k GB-s/day) ולהחליט מתי $5 מוצדק.
- סיימת את פרק 1 ויש לך פרויקט עם Worker חי,
wrangler.tomlבסיסי עם בלוק[assets], ו-wranglerמותקן ומחובר (wrangler login/whoamiעוברים). - טבלת ה-free-tier matrix מפרק 1 שמורה בצד — נרחיב אותה כאן לטבלת החלטה ולמחשבון.
- נוחות בהרצת פקודות
npx wrangler ...בטרמינל ועריכת קובץwrangler.toml. - יכולת לקרוא TypeScript/JavaScript (לא לכתוב מאפס) — ה-AI יכתוב את ה-handlers; אתה מחליט לאיזה primitive הם פונים.
הפרק הקודם (פרק 1): פרסת Worker עם Static Assets על *.workers.dev, יש לך wrangler.toml בסיסי, .dev.vars לסודות לוקליים, וטבלת free-tier matrix ביד.
הפרק הזה: ממשיכים על אותו פרויקט בדיוק — מוסיפים storage bindings ל-wrangler.toml, מפצחים את קיר ה-10ms, ובוחרים את ה-primitive הנכון לכל בעיה. בלי להתחיל מאפס.
הפרק הבא (פרק 3 — Workers AI ו-RAG): נוסיף לאותו wrangler.toml binding של Workers AI ו-Vectorize, ונשתמש מחדש ב-D1 (metadata של מסמכי ה-RAG) וב-KV (cache / rate-limiting) שתקים כאן. הפרק חייב להסתיים עם 4 bindings פעילים — זו פלטפורמת הזינוק של פרק 3.
| מונח | בעברית | הסבר |
|---|---|---|
| CPU time | זמן עיבוד | הזמן שבו ה-CPU בפועל מריץ את קוד ה-Worker שלך. המגבלה החינמית היא 10ms. המתנה על fetch/KV/D1 לא נספרת — רק חישוב מקומי (parse, regex, לולאות) שורף CPU time. שונה מ-wall-clock (הזמן הכולל שחלף). |
| wall-clock time | זמן קיר/שעון | סך כל הזמן שחלף מתחילת הבקשה ועד סופה, כולל המתנה לרשת. אין hard limit עליו בבקשת HTTP רגילה כל עוד הלקוח מחובר; waitUntil() מאריך עד 30s אחרי disconnect. |
| subrequest | תת-בקשה | כל קריאת רשת יוצאת מ-Worker (fetch, KV, D1, R2, AI). חינם: 50/invocation; בתשלום: 10,000. בנוסף יש cap של 6 חיבורים יוצאים בו-זמנית לכל request בשני הטיירים. |
| Workers KV | אחסון מפתח-ערך | אחסון key-value מבוזר ו-edge-cached, eventually consistent. חינם: 100k reads, 1,000 writes/day (ל-keys שונים), 1GB. מתאים ל-config, feature flags, sessions, cached lookups. לא לכתיבה כבדה. |
| eventual consistency | עקביות מתכנסת | מודל עקביות שבו כתיבה מתפשטת גלובלית בהדרגה — קריאה עלולה להחזיר ערך מעט ישן עד ש-cacheTtl פג (מינימום 30s מאז 2026-01-30, ירד מ-60s). מאפיין את KV; ההפך מ-strongly consistent. |
| D1 | מסד SQLite מנוהל | מסד SQLite מנוהל של Cloudflare, strongly consistent עם primary יחיד לכתיבות ו-read replicas opt-in (Sessions API). חינם: 5M rows read/day, 100k written/day, 5GB. מתאים לנתונים רלציוניים. |
| rows read | שורות שנסרקו | מספר השורות ששאילתת D1/DO סורקת — לא שמוחזרות. SELECT * full-scan של 5,000 שורות = 5,000 rows read. JOIN/GROUP BY מכפילים; index מפחית. ה-budget החינמי 5M/day. |
| R2 | אחסון אובייקטים | אחסון אובייקטים תואם-S3 עם $0 egress בכל הטיירים — המבדיל המרכזי מול AWS S3. חינם: 10GB, 1M Class A, 10M Class B ops/חודש (Standard storage בלבד). מתאים ל-blobs, uploads, גיבויים. |
| Class A / Class B | סיווג פעולות R2 | סיווג חיוב ב-R2: Class A = פעולות שמשנות state (PutObject, CopyObject, ListObjects) — יקרות יותר, 1M/חודש חינם. Class B = קריאה (GetObject, HeadObject) — 10M/חודש חינם. DeleteObject חינם לגמרי. |
| Durable Objects | אובייקטים יציבים | אובייקטים stateful, single-threaded ו-globally addressed — ה-primitive היחיד ב-Cloudflare ל-real-time coordination (WebSocket rooms, presence, נעילות). single-threaded = גישה מסודרת = strong consistency פר-instance. |
| SQLite backend (DO) | מנגנון אחסון של DO | מנגנון האחסון של DO שזמין בחינם ב-2026 (KV backend הוא paid-only). מצהירים עליו עם new_sqlite_classes בבלוק [[migrations]], לא new_classes. |
| GB-seconds | גיגה-שניות | יחידת חיוב duration של DO: זיכרון (GB) × זמן פעיל (שניות). חינם: 13,000 GB-s/day. DO ב-128MB שטעון שעה ≈ 460 GB-s. DO שזכאי ל-hibernation לא צובר GB-s כשהוא idle. |
| Hibernation API | מנגנון תרדמת | מנגנון של Durable Objects (Hibernatable WebSockets) שמאפשר ל-DO idle לא לצבור duration/GB-s גם כשחיבורי WebSocket פתוחים אך לא פעילים. הפתרון למלכודת ה-"long-lived hub eats GB-s". |
| Workflows | תהליכים יציבים | מנוע durable multi-step עם retry אוטומטי ו-state מתמיד בין צעדים — מחליף state machines ידניים ב-DO. חינם: 1,024 steps/workflow, 10ms/step. בתשלום הועלה ב-2026-03-03 ל-10,000 (עד 25,000). |
| Queues | תורי הודעות | תור הודעות מנוהל לעיבוד אסינכרוני. חינם מ-2026-02-04: 10,000 ops/day, retention 24h לא-נשלט. 1 op לכל 64KB; הודעה טיפוסית = 3 ops (write+read+delete); כל retry/DLQ = op נוסף. |
| Cron Triggers | טריגרים מתוזמנים | תזמון הרצות Worker בלוח cron. חינם: 5/account (250 בתשלום), 10ms CPU. כל הפעלה נספרת מול מכסת 100k ה-requests היומית; wall time עד 15 דקות להפעלה. |
| Hyperdrive | מאיץ DB חיצוני | connection pooler + cache ל-DB חיצוני (Postgres/MySQL כמו Supabase/Neon). כלול ב-Free ו-Paid ללא חיוב per-query; חינם 100k queries/day. מלכודת: שאילתות מ-cache נספרות זהה ל-uncached. |
| wrangler secret put | שמירת סוד production | פקודה ששומרת סוד production מוצפן (wrangler secret put NAME, מבקשת ערך). שונה מ-[vars] ב-wrangler.toml שהוא plaintext+committed, ושונה מ-.dev.vars שהוא לוקלי בלבד. |
| wrangler tail | לוגים חיים | פקודה שמזרימה לוגים חיים מ-Worker שרץ ב-production — לדיבוג בזמן אמת של בקשות אמיתיות, כולל שגיאות ומדידות CPU שלא תמיד מופיעות ב-wrangler dev הלוקלי. |
למה הפרק הזה: מ-"Worker חי" ל-"Worker שזוכר דברים"
בסוף פרק 1 היה לך Worker חי על *.workers.dev — אבל הוא אמנזי. כל בקשה מתחילה מאפס; הוא לא זוכר מי נכנס, מה הזין, או מה קרה בבקשה הקודמת. ברגע שתרצה לבנות משהו אמיתי — login, רשימת משתמשים, העלאת קובץ, צ'אט בזמן אמת — אתה צריך state: מקום שבו נתונים שורדים בין בקשות.
הפרק הזה עונה על שתי שאלות שכל אפליקציה stateful נופלת עליהן, ושתיהן קובעות אם תישאר ב-$0 או תיאלץ לשלם:
- כמה חישוב מותר לי? זה "קיר ה-10ms" — מגבלת ה-CPU החינמית. נראה שהיא הרבה יותר נדיבה ממה שזה נשמע, אבל יש כמה דברים ספציפיים שמפוצצים אותה.
- איפה אני שם את ה-state? ל-Cloudflare יש ארבעה primitives —
KV,D1,R2,Durable Objects— וכל אחד נכון לבעיה אחרת. בחירה שגויה לא רק איטית; היא שורפת לך את המכסה החינמית הצרה ביותר תוך יום.
הגישה כאן היא מודל-מנטלי-לפני-קוד. לפני שתכתוב env.DB.prepare(...) כלשהו, תדע בדיוק למה בחרת D1 ולא KV, ואיזו מגבלה תיפול ראשונה כשהאפליקציה תצמח. ה-AI (Claude Code / Cursor) יכתוב את ה-handlers בשבילך — אבל הוא לא ידע לבד שאתה עומד לשבור את מכסת 1,000 הכתיבות של KV. ההחלטה הזו שלך, והפרק הזה נותן לך אותה כמיומנות, לא כניחוש.
ולמה דווקא ארבעה primitives ולא אחד? כי "אחסון" זו לא בעיה אחת — אלה ארבע בעיות שונות שנראות דומות. שמירת הגדרות שכמעט לא משתנות זה לא אותו דבר כמו מסד נתונים רלציוני, שזה לא אותו דבר כמו אחסון קבצים גדולים, שזה לא אותו דבר כמו תיאום בזמן אמת בין כמה משתמשים. כל אחד מהארבעה מותאם לאחת מהבעיות האלה, ובחירה שגויה לא רק מבזבזת — היא לפעמים בלתי אפשרית (אי-אפשר לעשות counter מדויק תחת תחרות ב-KV, נקודה). בסוף הפרק תזהה את הבעיה ממבט אחד ותדע איזה כלי שולפים.
שים לב למבנה הפרק: קודם שתי שאלות ה-compute (קיר ה-10ms ו-subrequests), אחר כך ארבעת ה-primitives של אחסון אחד-אחד, אחר כך שלושת ה-primitives האסינכרוניים (Workflows/Queues/Cron) ו-Hyperdrive ל-DB חיצוני, ולבסוף ה-debug-workflow ו-"מה קורה בקיר". כל סעיף בונה על הקודם, ושלושת התרגילים המרכזיים — טבלת החלטה, wrangler.toml עם 4 bindings, ומחשבון מגבלות — הם ה-deliverables שתסיים איתם ביד.
הערה על המספרים: Cloudflare משנה מגבלות תכופות. כל מספר בפרק אומת מול התיעוד הרשמי בתאריך הכנת הקורס (מאי 2026), והסעיפים הרגישים מסומנים. לפני בנייה כבדה — תמיד שווה לאמת מול developers.cloudflare.com.
פתח את תיקיית הפרויקט מפרק 1 והרץ npx wrangler dev. ודא שה-Worker עדיין עולה ב-localhost:8787. זה ה-Worker שנוסיף לו storage bindings לאורך כל הפרק — אנחנו ממשיכים עליו, לא מתחילים מחדש.
קיר ה-10ms: CPU time מול wall-clock, ומה באמת שורף את התקציב
השם "קיר ה-10ms" מפחיד מתחילים: "10 אלפיות שנייה? כל קריאת רשת לוקחת פי 50 מזה — איך אפשר לעשות משהו?". זה בדיוק אי-ההבנה שעולה כסף. המגבלה היא CPU time, לא wall-clock time, ושני אלה דברים שונים לחלוטין.
בלשון התיעוד הרשמי: "CPU time מודד כמה זמן ה-CPU מבלה בהרצת קוד ה-Worker שלך. המתנה לבקשות רשת (כמו fetch(), קריאות KV, או שאילתות מסד נתונים) לא נספרת ב-CPU time." כלומר: בזמן ש-await fetch(...) מחכה 500ms לתשובה מהרשת, ה-CPU שלך פנוי — אותו זמן המתנה הוא wall-clock, לא CPU. הקיר של 10ms נוגע רק לזמן שבו המעבד באמת מחשב משהו.
חשוב על זה כמו טבח במטבח. ה-wall-clock הוא כמה זמן הלקוח מחכה לצלחת — כולל הזמן שהתנור עובד. ה-CPU time הוא רק כמה זמן הטבח עבד בידיו. אם המנה בתנור 40 דקות, הטבח לא עומד מולה — הוא מכין מנות אחרות. אותו דבר ב-Worker: בזמן שה-fetch "בתנור", ה-CPU פנוי לבקשות אחרות. Cloudflare מחייב אותך על עבודת הידיים (CPU), לא על זמן התנור (רשת). לכן 10ms זה הרבה יותר ממה שזה נשמע — זה 10ms של חישוב, וברוב ה-Workers רוב הזמן הולך בכלל בהמתנה.
אז מה כן שורף את ה-10ms? כל חישוב מקומי סינכרוני:
JSON.parseעל payload גדול — לפרסר 50KB של JSON זה עבודת CPU אמיתית.- regex על עשרות KB — ביטוי רגולרי כבד על טקסט ארוך יכול לבדו לחצות 10ms.
- לולאות ארוכות — מעבר על מערך של מאות אלפי איברים, מיון, אגרגציה ידנית.
- hashing / crypto כבד — חישוב hash על נתונים גדולים, הצפנה/פענוח.
הכלל המעשי: אם הקוד מחכה — הוא חינם. אם הקוד מחשב — הוא נספר. פצצת הזמן הקלאסית היא קוד שגודל ה-payload שלו גדל עם הקלט: parse שעובד מצוין על 2KB בפיתוח, ומתפוצץ על 200KB בפרודקשן.
ומה ה-wall-clock? אין עליו hard limit בבקשת HTTP רגילה כל עוד הלקוח מחובר. אם תרצה להמשיך עבודה אחרי שהחזרת תשובה ללקוח (למשל לכתוב לוג או לעדכן cache), ctx.waitUntil(promise) מאריך את חיי ה-Worker עד 30 שניות אחרי ה-disconnect — בלי להחזיק את הלקוח ממתין.
ב-Paid הקיר רחב בהרבה: 30 שניות CPU כברירת מחדל (ניתן עד 5 דקות). לכן קיר ה-10ms הוא ה-forcing function הראשון ל-$5: ברגע שיש לך compute לא-טריוויאלי שלא ניתן לדחוף ל-Queue/Workflow, השדרוג מצדיק את עצמו.
הנה הויזואליזציה של ההבדל — אותה בקשה, שני סוגי זמן:
שאל על כל קטע קוד:
- הוא
awaitעל I/O? (fetch / KV / D1 / R2 / AI) → לא נספר ב-CPU. אל תדאג מהאיטיות; ההמתנה חינמית. - הוא חישוב מקומי סינכרוני? (
JSON.parseעל payload גדול, regex על עשרות KB, לולאה ארוכה, hashing/crypto) → שורף CPU. ואם ה-payload גדל עם הקלט — זו פצצת זמן ל-10ms.
החלטה כשזה שורף:
- דחוף את ה-parsing הכבד ל-
Queue/Workflow(עיבוד אסינכרוני, מחוץ לבקשה). - או צמצם את גודל ה-payload (paginate, סנן בצד ה-DB, אל תביא הכל).
- או — אם החישוב מהותי ולא ניתן לדחיה — שדרג ל-$5 (30s CPU).
מקרה גבול: streaming/transform על stream נספר תוך כדי זרימה — מדוד אותו אמיתית עם wrangler tail, אל תנחש.
למה זה מפתה: כמה בלוגים של 2026 כותבים "50ms (free)" ומתכננים לפיו. המספר נשמע סמכותי.
למה זה טעות: ה-50ms היה ה-cap של מודל ה-Bundled (Paid) הישן, שהוחל אוטומטית על Workers שנוצרו לפני מרץ 2024. זו לא המגבלה החינמית, ולא רלוונטית ל-Worker חדש ב-2026. החינמי הוא 10ms CPU (execution time בלבד).
מה לעשות במקום: תכנן ל-10ms CPU. זכור שזה execution time — await על fetch/KV/D1 חינם, אבל JSON.parse על payload גדול או regex כבד שורפים אותו. אמת מול developers.cloudflare.com/workers/platform/limits/ לפני שאתה מסתמך על מספר.
הוסף ל-Worker שורה: console.log('cpu test') ואז await fetch('https://example.com'). הרץ npx wrangler dev, פנה ל-Worker, וצפה בלוג. שים לב שה-await על fetch לא מקריס אותך — זמן הרשת הוא לא CPU time.
Subrequests: 50 חינם מול 10,000 בתשלום — ו-6 חיבורים במקביל
כל קריאת רשת יוצאת מ-Worker היא subrequest — וזה כולל לא רק fetch לשרתים חיצוניים, אלא גם כל פנייה ל-KV, D1, R2 או Workers AI. כל אלה נספרים מול אותה מכסה.
| מגבלה | Free | Paid |
|---|---|---|
| Subrequests לכל invocation | 50 | 10,000 |
| חיבורים יוצאים בו-זמנית לכל request | 6 | 6 |
שים לב לעמודה השנייה: ה-cap של 6 חיבורים יוצאים בו-זמנית זהה בשני הטיירים. זה לא אותו דבר כמו ה-50 subrequests — אתה יכול לעשות 50 קריאות סך-הכל, אבל לא יותר מ-6 מהן פתוחות באותו רגע.
זה משנה איך אתה כותב fan-out. יש שני דפוסים:
- cascade (סדרתי):
awaitבתוך לולאה — כל קריאה ממתינה לקודמתה. 3 קריאות של 200ms = 600ms wall-clock. בטוח, אבל איטי. - Promise.all (מקבילי): משגרים את כל הקריאות יחד ומחכים לכולן. 3 קריאות של 200ms ≈ 200ms wall-clock. מהיר בהרבה — אבל מעבר ל-6 בו-זמנית, Cloudflare מתחיל לתור אותן בגלל ה-cap.
הכלל: השתמש ב-Promise.all כשהקריאות בלתי-תלויות (כל אחת עומדת בפני עצמה), אבל אל תנסה לפתוח 30 חיבורים במקביל ולצפות לפי-30 שיפור — ה-6-connection cap יבלם אותך. ל-fan-out רחב באמת, batch של 6 בכל גל.
למה זה חשוב לך כ-Vibe Coder? כי ה-AI אוהב לכתוב Promise.all על מערך שלם בלי לחשוב על ה-cap. אם ביקשת "תביא נתונים מ-10 מקורות במקביל", הקוד שתקבל ינסה לפתוח 10 חיבורים בו-זמנית — והם פשוט ייתורו בשקט מעבר ל-6. לא תקבל שגיאה; תקבל ביצועים פחות טובים ממה שציפית, בלי להבין למה. כשאתה יודע על ה-cap, אתה יודע לבקש batching, או להבין שהמספרים שאתה רואה ב-wrangler tail הגיוניים.
טעות מנטלית נפוצה כאן: לבלבל בין ה-50 subrequests (מכסה כוללת לכל invocation) ל-6 החיבורים (כמה פתוחים בו-זמנית). אתה יכול לעשות 50 קריאות לאורך כל הבקשה — רק לא יותר מ-6 חיות באותו רגע. דמיין תור בקופה: 6 קופות פתוחות (החיבורים), ו-50 לקוחות שמותר להם לעבור בסך-הכל (ה-subrequests). הקופות הן הצוואר, לא מספר הלקוחות.
בקוד ה-Worker, כתוב לולאה שמבצעת 3 fetch עם await בתוך הלולאה (סדרתי). לידה כתוב גרסה עם Promise.all של אותם 3 fetch (מקבילי). אל תריץ — רק סמן לעצמך מי מהן מהירה יותר ולמה (רמז: cap 6 חיבורים).
KV — ה-edge cache: eventually consistent, read-heavy, ומלכודת 1,000 הכתיבות
Workers KV הוא אחסון key-value מבוזר. הדבר היחיד שצריך להפנים עליו: הוא בנוי ל-קריאות, לא ל-כתיבות. אחרי קריאה ראשונה, הערך נשמר ב-edge cache קרוב למשתמש, וקריאות הבאות מהירות מאוד. אבל המודל שלו הוא eventually consistent — כתיבה מתפשטת גלובלית בהדרגה, וקריאה עלולה להחזיר ערך מעט ישן עד שה-cacheTtl פג.
המספרים החינמיים:
| מגבלה | Free | הערה |
|---|---|---|
| Reads/day | 100,000 | קריאה bulk נספרת כ-1 |
| Writes/day | 1,000 | ל-keys שונים |
| Deletes/day | 1,000 | |
| List requests/day | 1,000 | |
| Storage | 1GB | value עד 25MiB, key עד 512B |
| כתיבה לאותו key | 1/שנייה | cap נפרד, שני הטיירים |
שים לב לאסימטריה: 100,000 קריאות מול 1,000 כתיבות — פי 100. זה לא מקרי; זה אומר לך בדיוק לְמה KV מיועד. ה-use-cases הקלאסיים: config, feature flags, sessions, cached lookups, וקריאת secrets server-side (הדפוס cf-kv.ts: קריאה דרך REST API עם Bearer token). כל אלה נכתבים לעיתים רחוקות ונקראים הרבה.
בוא נעשה את האסימטריה הזו קונקרטית. נניח שאתה שומר ב-KV את הגדרות האפליקציה (feature flags) — אובייקט JSON אחד תחת key אחד. כל בקשה של כל משתמש קוראת אותו כדי לדעת אילו פיצ'רים פעילים: עם 100,000 משתמשים ביום זה 100,000 reads — בדיוק במכסה, וזה בסדר כי קריאות זולות. אבל אתה כותב את ה-config רק כשאתה משנה הגדרה — כמה פעמים בשבוע. זה הדפוס שבו KV זוהר: write-once-read-many. הפוך אותו — כתיבה בכל בקשה — והוא קורס.
למה בכלל יש cap כתיבה לאותו key (1/שנייה)? כי KV הוא מבוזר: כתיבה צריכה להתפשט לכל ה-edge nodes, ושתי כתיבות צמודות לאותו key היו יוצרות race שאי-אפשר לפתור eventually. ה-cap הזה הוא תזכורת מבנית — KV הוא לא מקום ל-state שמשתנה במהירות. אם אתה מוצא את עצמך נלחם ב-cap הזה, הסימן ברור: עברת מ-config ל-state, ואתה צריך DO או D1.
נקודה טכנית שימושית: קריאה מסוג get() או getWithMetadata() מקבלת פרמטר cacheTtl — כמה שניות לשמור את הערך ב-edge cache לפני שמרעננים מהמקור המרכזי. ערך גבוה = פחות עדכניות אבל פחות latency; ערך נמוך (מינימום 30s מאז 2026-01-30) = יותר עדכניות. ל-config שמשתנה נדיר, cacheTtl גבוה הוא בחירה טובה.
לגבי ה-staleness: ה-propagation הוא בדרך כלל עד ~60s, חסום ע"י ה-cacheTtl שאתה מגדיר. ה-minimum cacheTtl ירד מ-60s ל-30s ב-2026-01-30 — כלומר ערך חדש יכול להיראות מהר יותר מבעבר, אבל אל תתייחס לזה כ-SLA קשיח. אם אתה צריך לראות כל כתיבה מיד ובוודאות — KV הוא הכלי הלא נכון; זה D1 או Durable Object.
החיבור ב-wrangler.toml:
[[kv_namespaces]]
binding = "MY_KV"
id = "<your-kv-namespace-id>"
למה זה מפתה: "KV זה key-value, אז אשמור שם מונה ואגדיל אותו בכל request" — נשמע פשוט.
למה זה טעות: KV הוא eventually consistent ומוגבל ל-1,000 writes/day ל-keys שונים, ועוד cap נפרד של כתיבה אחת/שנייה לאותו key. "הגדל מונה בכל request" פוגע בשני הקירות בבת אחת: גם בקצב לאותו key, וגם במכסה היומית.
מה לעשות במקום: counter/state עקבי תחת תחרות שייך ל-Durable Object (single-threaded, strongly consistent). נתון רלציוני שייך ל-D1. השאר ב-KV רק config/feature-flags/sessions/cached-lookups שנכתבים לעיתים רחוקות.
הרץ npx wrangler kv namespace create MY_KV. העתק את ה-id שחזר. אל תוסיף עדיין ל-wrangler.toml — נעשה את זה בתרגיל ה-bindings. רק שמור את ה-id בצד.
D1 — SQLite ב-edge: strongly consistent, ומלכודת ה-rows-read
D1 הוא מסד SQLite מנוהל. בניגוד ל-KV, הוא strongly consistent: כל כתיבה הולכת ל-primary יחיד, וקריאה אחריה תראה אותה תמיד. (יש read replicas, אבל הם opt-in דרך ה-Sessions API — לא מופעלים אוטומטית; ברירת המחדל היא primary יחיד עם עקביות מלאה.) זה ה-primitive לנתונים רלציוניים/structured: רשומות משתמשים, טבלאות עם קשרים, נתונים שאתה רוצה לתשאל עם SQL.
המספרים החינמיים:
| מגבלה | Free |
|---|---|
| Rows read/day | 5,000,000 |
| Rows written/day | 100,000 |
| Storage כולל | 5GB |
| גודל DB | 500MB |
| מספר DBs | 10 |
וכאן המלכודת הגדולה — rows read זה שורות שנסרקו, לא שורות שהוחזרו. בלשון התיעוד: "Rows read מודד כמה שורות שאילתה קוראת (סורקת), ללא קשר לגודל כל שורה. לדוגמה, אם יש טבלה עם 5,000 שורות ומריצים SELECT * FROM table כ-full table scan, זה נספר כ-5,000 rows read."
כלומר: גם אם השאילתה החזירה שורה אחת, אם היא נאלצה לסרוק 5,000 כדי למצוא אותה — שילמת 5,000 rows read. ו-JOIN/GROUP BY מכפילים את זה מהר. דיווח אמיתי מהקהילה: 28,000 רשומות, 117 שאילתות → יותר מ-90 מיליון rows read — כי כל JOIN סרק את כל הטבלאות.
הפתרון הוא בסיסי SQL טוב: תמיד WHERE, ו-index על העמודות שאתה מסנן/מצרף לפיהן. index מפחית את ה-rows read בקריאה דרמטית (הוא מאפשר ל-SQLite לקפוץ ישר לשורה במקום לסרוק). יש לו מחיר: index מוסיף שורה כתובה לכל עמודה מאונדקסת בכל כתיבה — אבל זה מחיר זול לעומת לרוקן 5M rows read מ-full-scans.
בוא נחדד את ה-rows-read עם דוגמה. טבלת users עם 50,000 שורות. אתה מריץ SELECT * FROM users WHERE email = ?:
- בלי index על
email: SQLite חייב לעבור על כל 50,000 השורות כדי לבדוק אילו מתאימות — גם אם רק אחת מתאימה. עלות: 50,000 rows read לשאילתה אחת. עם 100 חיפושים כאלה ביום כבר נגסת 5M — כל המכסה. - עם index על
email: SQLite קופץ ישר לשורה הנכונה דרך ה-index. עלות: כמה rows read בודדים. אותה שאילתה, פי-עשרת-אלפים פחות.
ההבדל הזה הוא בדיוק למה "תמיד index על מה שאתה מחפש לפיו" הוא לא טיפ לביצועים בלבד — זה טיפ ל-תקציב. ב-free tier זה ההבדל בין app שעובד ל-app שמפסיק להגיב אחה"צ. כשאתה לא בטוח למה שאילתה איטית או יקרה, הרץ EXPLAIN QUERY PLAN לפניה — אם אתה רואה SCAN TABLE במקום SEARCH ... USING INDEX, חסר לך index.
מתי לא D1? כשהנתון לא רלציוני (blob → R2), כשאתה צריך real-time coordination (counter תחת תחרות → DO), או כשהנתון read-heavy וקטן ונסבל קצת stale (config → KV). D1 מצוין כשיש לך טבלאות, קשרים, ושאילתות — בדיוק כמו כל מסד SQL שאתה מכיר, רק על ה-edge.
החיבור ב-wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "<your-d1-database-id>"
SELECT בלי WHERE על טבלת D1 גדולה
למה זה מפתה: "אביא את כל הרשומות ואסנן בקוד" — קל לכתוב, עובד מצוין על 50 שורות בפיתוח.
למה זה טעות: rows read סופר כל שורה שהשאילתה סורקת. full-scan של 5,000 שורות = 5,000 rows read גם אם השתמשת באחת. בפרודקשן עם עשרות אלפי שורות ו-JOINs, אתה מרוקן את 5M ה-rows-read בשקט — ובניגוד לחשבון מפחיד, ה-free tier פשוט מפסיק לעבוד עד reset.
מה לעשות במקום: תמיד סנן עם WHERE והוסף index על העמודות שאתה מסנן/מצרף לפיהן. תן ל-DB לעשות את הסינון, לא לקוד. בדוק EXPLAIN QUERY PLAN כשמשהו איטי.
הרץ npx wrangler d1 create my-app-db. שמור את ה-database_id שחזר. ואז npx wrangler d1 execute my-app-db --command "SELECT 1" כדי לוודא שהיא חיה ומגיבה.
R2 — אחסון blob ב-$0 egress: Class A מול Class B, ולמה List יקר
R2 הוא אחסון אובייקטים תואם-S3 — לקבצים גדולים: תמונות, וידאו, גיבויים, uploads של משתמשים. ה-killer feature שלו מול AWS S3 הוא $0 egress: אתה לא משלם על הוצאת נתונים. ב-S3 ה-egress הוא לעיתים העלות הגדולה ביותר; ב-R2 הוא אפס בכל הטיירים. בשביל מי שמגיש קבצים ללקוחות, זה חוסך הון.
המספרים החינמיים (Standard storage בלבד):
| מגבלה | Free |
|---|---|
| Storage | 10GB-month |
| Class A ops/month | 1,000,000 |
| Class B ops/month | 10,000,000 |
| Egress | חינם ($0) |
החיוב מתחלק לשני סוגי פעולות, ו-זו ההבחנה שתופסת אנשים:
- Class A (פעולות שמשנות state, יקרות יותר — 1M/חודש): PutObject, CopyObject,
ListObjects, CreateMultipartUpload, UploadPart... - Class B (קריאה — 10M/חודש): GetObject, HeadObject, GetBucket*...
- חינם לגמרי (לא A ולא B): DeleteObject, DeleteBucket, AbortMultipartUpload.
ה-twist: ListObjects הוא Class A, לא Class B. רוב האנשים מניחים ש"לרשום קבצים" זו פעולת קריאה זולה — אבל היא נספרת מול ה-1M החודשי הצר, לא מול ה-10M הרחב. אם ה-read-path שלך עושה list() על bucket בכל בקשה (למשל "תביא את כל התמונות של המשתמש"), אתה שורף את המכסה היקרה במהירות.
תכנן את ה-read-path סביב GetObject ישיר (לפי key ידוע) ולא סביב List חוזר. אם אתה צריך לדעת מה יש למשתמש — שמור את רשימת ה-keys ב-D1 או KV, ופנה ל-R2 ישירות לפי key.
זה דפוס ארכיטקטוני שחוזר הרבה ב-Cloudflare: R2 מחזיק את ה-bytes, ו-D1/KV מחזיק את ה-metadata. נניח אפליקציה שמאחסנת תמונות פרופיל. אתה לא עושה list() על ה-bucket כדי למצוא את התמונה של משתמש; אתה שומר ב-D1 שורה users(id, avatar_key), קורא ממנה את ה-avatar_key, ואז env.BUCKET.get(avatar_key) ישירות — GetObject אחד (Class B, זול), אפס List. ה-metabase יודע איפה הדברים; R2 רק מגיש אותם.
עוד נקודה ש-Vibe Coders מפספסים: DeleteObject חינם לגמרי — לא Class A ולא Class B. אז למחוק קבצים ישנים (cleanup, lifecycle) לא עולה לך quota. מה שכן עולה הוא הכתיבה (Put, Class A) וה-List. זכור את ה-egress: $0 בכל הטיירים. אם אתה מגיש וידאו או תמונות כבדות ללקוחות — זה המקום שבו R2 חוסך לך פי-עשרות מול S3, שבו ה-egress הוא לרוב העלות הראשית.
החיבור ב-wrangler.toml (לפי שם, לא id):
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-app-bucket"
הרץ npx wrangler r2 bucket create my-app-bucket. ודא שהוא נוצר ברשימה עם npx wrangler r2 bucket list. שמור את שם ה-bucket לחיבור ב-wrangler.toml.
Durable Objects — ה-primitive היחיד ל-real-time: single-threaded, SQLite חינם, ו-Hibernation
שלושת ה-primitives הקודמים (KV, D1, R2) הם אחסון. Durable Objects (DO) הוא משהו אחר: אובייקט stateful עם קוד שרץ בו, single-threaded, ו-globally addressed (לכל instance יש ID ייחודי ומיקום אחד). ה-single-threaded הוא הקסם: כי רק thread אחד ניגש ל-state בכל רגע, אתה מקבל גישה מסודרת = strong consistency + coordination בלי לנהל נעילות בעצמך.
זה ה-primitive היחיד ב-Cloudflare ל-real-time אמיתי: WebSocket rooms (צ'אט, משחקים), presence ("מי מחובר עכשיו"), נעילה פר-משתמש/חדר, counter מדויק תחת תחרות, state machines.
למה ה-single-threaded הוא העניין כולו? קח את ה-counter שדיברנו עליו ב-KV. ב-KV, אם שני משתמשים מגדילים מונה בו-זמנית, שניהם קוראים "5", שניהם כותבים "6" — איבדת ספירה. זו בעיית race classic. ב-DO זה לא יכול לקרות: כי thread אחד בלבד ניגש ל-state, הבקשה השנייה מחכה שהראשונה תסיים. אחד קורא 5 כותב 6, השני קורא 6 כותב 7. מדויק, בלי שתכתוב נעילה אחת בעצמך. זה מה ש-"strong consistency + coordination" אומר בפועל — ה-runtime עושה את הסידור בשבילך.
וה-globally addressed? לכל DO יש ID, ואתה מקבל אליו handle לפי שם (למשל שם החדר: env.ROOM.idFromName("room-42")). כל הבקשות ל-"room-42" מנותבות לאותו instance יחיד בעולם — לכן כולם בחדר רואים את אותו state. זה בדיוק מה שהופך WebSocket rooms לאפשריים: חדר = DO, וכל מי שמחובר אליו מדבר עם אותו אובייקט.
נקודה קריטית ל-2026: ב-DO יש שני backends לאחסון — SQLite ו-KV. ה-SQLite backend הוא היחיד שזמין בחינם; ה-KV backend הוא paid-only. לכן כל מחלקת DO חדשה שלך חייבת להצהיר על SQLite backend, וזה נעשה עם new_sqlite_classes בבלוק ה-[[migrations]] — לא new_classes (זה ה-KV backend, וייכשל ביצירה ב-free plan).
המספרים החינמיים:
| מגבלה | Free |
|---|---|
| Requests/day | 100,000 (HTTP + RPC + WebSocket messages + alarms) |
| Duration | 13,000 GB-s/day |
| SQLite rows read/day | 5,000,000 |
| SQLite rows written/day | 100,000 |
| Storage | 5GB (account-total) |
שים לב: ה-rows read/written של ה-SQLite backend תואמים בדיוק את D1 — אותו 5M/100k, ואותה מלכודת בדיוק. כל מה שלמדת על rows-read ב-D1 חל מילה במילה על אחסון של DO. אפילו ה-methods בסגנון KV (get/put/delete/list) על DO עם SQLite backend מאוחסנים בטבלת SQLite נסתרת ומחויבים כ-rows.
קיר ה-GB-seconds — והפתרון שרוב הבלוגים מפספסים
ה-duration נמדד ב-GB-seconds: זיכרון (GB) × זמן פעיל (שניות). DO ב-128MB שטעון שעה ≈ 0.128 × 3,600 ≈ 460 GB-s — כ-3.5% מהמכסה היומית. נשמע מסוכן ל-WebSocket hub שמחזיק חיבורים פתוחים שעות.
אבל הנה החלק הקריטי שמתפספס: "DO צובר חיובי duration כשהוא מבצע JS באופן פעיל או כשהוא idle אך לא עומד בתנאי ה-hibernation. DO idle שזכאי ל-hibernation לא צובר חיובי duration כלל."
כלומר: hub שמשתמש ב-Hibernation API (Hibernatable WebSockets) לא שורף 460 GB-s/שעה כשאף אחד לא מדבר — הוא צובר duration רק כשבאמת מעבדים הודעה. מלכודת ה-"long-lived hub eats GB-s" היא בעיה של DO לא-מתרדם בלבד. הפתרון הוא לא להימנע מ-WebSockets — הוא לכתוב אותם נכון עם Hibernation.
מה ההבדל בקוד? בלי Hibernation, ה-WebSocket מטופל ב-handler רגיל שחי כל עוד החיבור פתוח — וה-DO נשאר טעון בזיכרון, צובר GB-s. עם Hibernation API, אתה אומר ל-runtime "החזק את החיבור, אבל הרשה לי להירדם" — ה-runtime יכול לפנות את ה-DO מהזיכרון בין הודעות, ולהעיר אותו רק כשמגיעה הודעה חדשה. ה-state נשמר ב-SQLite, החיבור נשאר חי מבחינת המשתמש, אבל ה-DO לא "תופס מקום" כשהוא שותק. עבור צ'אט שבו אנשים מקלידים מדי פעם — זה ההבדל בין לשרוף את כל ה-13,000 GB-s לבין לגעת בשבר זעיר מהם.
הנקודה המעשית ל-Vibe Coder: כשאתה מבקש מה-AI לבנות WebSocket room, בקש במפורש Hibernatable WebSockets. הקוד "התמים" שתקבל אחרת יעבוד מצוין בפיתוח (חיבור אחד, אתה לבד) ויתחיל לשרוף GB-s בפרודקשן כשיש 50 חדרים פתוחים. זה בדיוק סוג ההחלטה שה-AI לא יקבל בשבילך — אבל אתה, אחרי הפרק הזה, כן.
החיבור ב-wrangler.toml:
[[durable_objects.bindings]]
name = "ROOM"
class_name = "Room"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["Room"]
שים לב — ל-DO אין פקודת create. בניגוד ל-KV / D1 / R2, אתה לא מריץ wrangler ... create ל-Durable Object ולא מקבל id להדביק. ה-DO מוגדר אך ורק על-ידי שני הבלוקים שלמעלה: [[durable_objects.bindings]] (שם ה-class) + [[migrations]] עם new_sqlite_classes = ["Room"] (זה מה שמפעיל את ה-SQLite backend החינמי). ה-instance עצמו נוצר לבד בקריאה הראשונה ל-env.ROOM.get(id) — אין שלב הקמה נפרד. בתרגיל 3 תכתוב את מחלקת ה-Room ותחווט אליה; אין כאן do-now של create כי אין מה ליצור ב-CLI.
למה זה מפתה: "WebSocket זה פשוט — אפתח חיבור ב-DO ואחזיק אותו". נראה תמים.
למה זה טעות: DO שמחזיק חיבורים פתוחים בלי Hibernatable WebSockets צובר GB-s גם כשאף אחד לא מדבר. hub של 128MB שטעון שעה idle ≈ 460 GB-s (3.5% מהיומי) — וזה מצטבר על פני כל החדרים הפעילים עד שהקיר נשבר.
מה לעשות במקום: השתמש ב-Hibernation API (Hibernatable WebSockets). DO idle שזכאי ל-hibernation לא צובר GB-s כלל — אתה צובר duration רק כשבאמת מעבדים הודעה. אותו hub, אותם משתמשים, אבל שבר זעיר מה-GB-s.
התחל מ-consistency ולך מטה:
- צריך coordination / real-time / נעילה? (WebSocket room, presence, counter מדויק תחת תחרות) → Durable Objects (SQLite backend, חינם; Hibernation למניעת GB-s).
- צריך strong consistency על נתונים רלציוניים / structured עם שאילתות? → D1 (תמיד
WHERE+ index כדי לא לרוקן rows-read). - read-heavy, נסבל קצת stale, וקטן? (config / feature-flag / session / secret) → KV (זכור 1,000 writes/day — לא ל-state פר-בקשה).
- זה blob? (תמונה / קובץ / גיבוי / upload) → R2 ($0 egress; תכנן סביב GetObject, הימנע מ-List חוזר).
כלל אצבע: כתיבה כבדה או counter פר-בקשה → לא KV. full-text/relational → D1. real-time/lock → DO. bytes גדולים → R2.
הטבלה הויזואלית הזו מסכמת את ההחלטה לפי צבע consistency (eventually = כתום, strong = ירוק):
בלי לכתוב קוד DO עדיין — פתח https://developers.cloudflare.com/durable-objects/ וחפש את המונח "Hibernatable WebSocket". כתוב לעצמך משפט אחד: "DO ב-hibernation לא צובר GB-s כי ___".
Workflows, Queues ו-Cron: עיבוד אסינכרוני ומתוזמן ב-$0
לא כל עבודה צריכה לקרות בתוך הבקשה. שלושה primitives נותנים לך עיבוד שקורה אחרי שהחזרת תשובה ללקוח — וכולם $0 בהיקפים סבירים. הם גם הפתרון הקלאסי לקיר ה-10ms: דחוף את העבודה הכבדה לכאן.
Workflows — durable multi-step
Workflows הוא מנוע צעדים יציב עם retry אוטומטי ו-state שמתמיד בין צעדים. אם יש לך תהליך רב-שלבי (קבל קובץ → עבד → שמור → שלח מייל), Workflows מבטיח שכל צעד רץ, נשמר, ומנסה שוב אם נכשל — בלי שתבנה state machine ידני ב-DO. המגבלה החינמית: 1,024 steps/workflow, 10ms CPU/step, 100MB state/instance, 100 concurrent, 100k executions/day (משותף עם Workers), retention 3 ימים.
נקודת freshness: ב-2026-03-03 ה-tier בתשלום הועלה מ-1,024 ל-10,000 default (עד 25,000). ה-free נשאר 1,024 — מספיק לרוב ה-pipelines; DAGs ארוכים באמת דורשים Paid.
Queues — תור הודעות
Queues חינמי מ-2026-02-04: 10,000 ops/day, retention 24h לא-נשלט. החיוב מבלבל: 1 op לכל 64KB, והודעה טיפוסית עוברת מחזור של 3 ops (write + read + delete) → כ-~3,300 הודעות פשוטות/יום. כל retry הוא op נוסף, וכל כתיבה ל-Dead Letter Queue נספרת. הודעה עד 128KB (מעל 64KB = כמה ops).
Cron Triggers — תזמון
Cron Triggers מריצים Worker בלוח זמנים (כל שעה, כל לילה ב-2:00...). חינם: 5/account (250 בתשלום), 10ms CPU, wall time עד 15 דקות להפעלה. שים לב: כל הפעלת cron נספרת מול מכסת 100,000 ה-requests היומית של Workers — cron שרץ כל דקה = 1,440 requests/יום מהמכסה.
איך לבחור ביניהם
קל להתבלבל בין השלושה. הנה ההבחנה המעשית: Cron זה "תריץ X בזמן Y" (גיבוי לילי, ניקוי שבועי) — מתוזמן בזמן. Queues זה "תעבד X מתישהו בקרוב, אחרי שהבקשה הסתיימה" (שלח מייל, עבד תמונה שהועלתה) — אסינכרוני אך לא מתוזמן. Workflows זה "תריץ X→Y→Z כסדרה אמינה עם retry בין הצעדים" (pipeline רב-שלבי שחייב להשלים) — multi-step עם זיכרון.
קשר חשוב לקיר ה-10ms: כל השלושה הם המוצא כשחישוב כבד מאיים על הבקשה. במקום לפרסר קובץ 5MB בתוך הבקשה ולחצות את ה-10ms — קבל את הקובץ, שמור ל-R2, דחוף הודעה ל-Queue, והחזר תשובה מיד. ה-consumer של ה-Queue יעבד את הקובץ ברקע (עם מכסת CPU משלו), והמשתמש לא ממתין. זה הדפוס שמאפשר ל-app על free tier לעבד עבודה כבדה בלי לשבור את הקיר.
למה זה מפתה: "התור יחזיק את ההודעות עד שה-consumer יקרא אותן" — נשמע כמו ההתנהגות הטבעית של תור.
למה זה טעות: ב-free tier ה-retention הוא 24 שעות לא-נשלט. אם ה-consumer שלך נפל ולא קם תוך יממה — ההודעות נמחקות, בלי אזהרה. בנוסף, כל retry וכל כתיבה ל-Dead Letter Queue נספרים כ-op נוסף מתוך ה-10,000 היומי.
מה לעשות במקום: תכנן את ה-consumer להיות עמיד (idempotent) ומנוטר. אם ההודעות קריטיות לטווח ארוך — שמור גם עותק ב-D1/R2, אל תסמוך על ה-retention של 24h. ל-retention ארוך יותר (עד 14 יום) צריך Paid.
בטבלת המטריצה מפרק 1, הוסף 3 שורות: Workflows, Queues, Cron Triggers. רשום ליד כל אחת את המגבלה החינמית (1,024 steps / 10k ops-day / 5 triggers). נרחיב אותן לטבלת ההחלטה בתרגיל.
Hyperdrive — pooler ל-Postgres/MySQL חיצוני, ומלכודת ה-cache-counts-the-same
מה אם כבר יש לך מסד Postgres או MySQL חיצוני — Supabase, Neon, PlanetScale — ואתה לא רוצה להגר ל-D1? כאן נכנס Hyperdrive: connection pooler + cache שיושב בין ה-Worker ל-DB החיצוני. הוא פותר את הבעיה ש-Workers פותחים המון חיבורים קצרים שמעמיסים על ה-DB, ומוסיף שכבת cache לשאילתות שניתן לשמור.
Hyperdrive כלול ב-Free וב-Paid ללא חיוב per-query. החינם: 100,000 queries/day; בתשלום unlimited.
המלכודת: "query" = כל statement דרך Hyperdrive — SELECT, INSERT/UPDATE/DELETE, או אפילו schema change. וקריטי: שאילתה שהוגשה מ-cache נספרת זהה ל-uncached. כלומר endpoint פופולרי עם caching מופעל יכול לרוקן את ה-100k היומי גם בלי לגעת ב-DB המקורי כלל — ה-cache מאיץ את התשובה, אבל לא חוסך לך quota.
איך זה מתבטא בפועל? נניח שיש לך endpoint /products שמריץ SELECT * FROM products מול Postgres חיצוני, עם caching מופעל. הביקור הראשון פוגע ב-DB; כל הביקורים הבאים מוגשים מה-cache של Hyperdrive — מהר מאוד, בלי לגעת ב-Postgres. נהדר ל-latency. אבל כל אחד מהם נספר מול ה-100k היומי. endpoint פופולרי עם 100,000 ביקורים ביום ירוקן את המכסה גם אם ה-Postgres שלך ישן כל היום. ה-cache מאיץ; הוא לא חוסך quota.
ה-connection string החי נשמר ב-Hyperdrive config (נוצר עם wrangler hyperdrive create), לא inline ב-wrangler.toml — שם רק ה-id מקושר:
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-config-id>"
Hyperdrive רלוונטי רק כשכבר יש לך Postgres/MySQL חיצוני שאתה לא רוצה להגר. לפרויקט חדש על Cloudflare — D1 הוא ברירת המחדל (מובנה, strongly consistent, אותו edge). Hyperdrive הוא גשר ל-DB קיים, לא תחליף ל-D1.
אם יש לך DB חיצוני (Supabase/Neon) — רשום את ה-connection string במקום בטוח. אם אין, רק רשום לעצמך: "Hyperdrive רלוונטי רק כשיש Postgres/MySQL חיצוני; אחרת D1". לא יוצרים Hyperdrive בתרגיל זה.
סודות ו-debug: wrangler secret put, wrangler tail, ומלכודות Miniflare מול --remote
ברגע שיש לך storage, יש לך גם סודות — מפתחות API, connection strings, tokens. יש שלוש דרכים לאחסן ערכי תצורה ב-Workers, ורק אחת נכונה לסוד production:
| מנגנון | מה זה | מתי |
|---|---|---|
[vars] ב-wrangler.toml | plaintext, נכנס ל-git | רק ערכי תצורה לא-סודיים (שם סביבה, feature flag). |
.dev.vars | dotenv לוקלי, gitignored | סודות בפיתוח לוקלי בלבד; נקרא רק ע"י wrangler dev. |
wrangler secret put | מוצפן ב-production | סוד production אמיתי. נשמר מוצפן, לא בקוד, לא ב-git. |
הכלל: סוד אמיתי לעולם לא ב-[vars] ולא בקוד. בפיתוח → .dev.vars; בפרודקשן → wrangler secret put NAME (מבקש את הערך, שומר מוצפן). שניהם נקראים בקוד דרך env.NAME — אותה גישה, מקור שונה.
Debug: wrangler tail מול wrangler dev
wrangler tail מזרים לוגים חיים מ-production — אתה רואה בקשות אמיתיות, שגיאות, ומדידות CPU כשהן קורות. זה הכלי לדבג בעיה שקורה רק בפרודקשן.
ו-wrangler dev (Miniflare) מדמה את KV/D1/R2/DO לוקלית על המחשב שלך — מהיר ונוח ל-CRUD רגיל. אבל הסימולציה לא מושלמת. שלוש התנהגויות מתפצלות מ-production:
- DO alarm timing — תזמון ה-alarms לא זהה לוקלית.
- KV TTL / propagation — ה-staleness וה-cacheTtl מתנהגים אחרת.
- R2 multipart upload resumption — חידוש העלאה מרובת-חלקים.
לכל אחד מאלה — smoke-test עם wrangler dev --remote, שרץ מול resources אמיתיים ב-edge במקום הסימולציה.
הרץ npx wrangler secret put DEMO_SECRET והזן ערך כלשהו (למשל hello). זהו סוד production מוצפן — שונה מ-.dev.vars הלוקלי. הרץ npx wrangler secret list לאמת שהוא קיים.
- ב-Worker, כתוב handler שמודד
Date.now()לפני ואחריawait fetch('https://example.com')ומחזיר את ההפרש ב-JSON (נתיב/fetch-test). - הוסף נתיב שני (
/parse-test): לולאה שמבצעתJSON.parseעל מחרוזת גדולה (~50KB) או regex כבד, ומדוד אותו זמן. - הרץ
npx wrangler devופנה לשני הנתיבים; רשום את שני הזמנים. - הסבר במשפט אחד למה ה-fetch (גם אם 500ms) לא מקריס את ה-10ms אבל ה-parse עלול.
- המר את נתיב ה-fetch הסדרתי לגרסת
Promise.allעם 3 fetch ורשום את ההבדל הצפוי (subrequests + 6-connection cap).
שלד מוכן להעתקה — התאם את התוכן, אל תכתוב מאפס:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/fetch-test") {
const t0 = Date.now();
await fetch("https://example.com"); // await על I/O = לא נספר ב-CPU
return Response.json({ route: "fetch", ms: Date.now() - t0 });
}
if (url.pathname === "/parse-test") {
const big = JSON.stringify(Array.from({ length: 20000 }, (_, i) => ({ i })));
const t0 = Date.now();
for (let i = 0; i < 50; i++) JSON.parse(big); // compute סינכרוני = שורף CPU
return Response.json({ route: "parse", ms: Date.now() - t0 });
}
return new Response("try /fetch-test or /parse-test");
},
};
פלט נראה לעין שתסיים איתו: Worker עם 2 נתיבים מדודים + פתק כתוב: "await על fetch = לא CPU; JSON.parse/regex = כן CPU" + הסבר מתי Promise.all עוזר. שמור צילום מסך של שתי התגובות עם הזמנים.
- פתח טבלה (Markdown/Sheets) עם עמודות: primitive | consistency | פרופיל latency | מחיר/מגבלה חינמית | המגבלה שנופלת ראשונה | use-case מובהק.
- מלא שורה ל-KV (eventually consistent, edge-cached, 1k writes/day ראשון ליפול, config/sessions).
- מלא שורה ל-D1 (strongly consistent, single-primary, rows-read scans ראשון ליפול, נתונים רלציוניים).
- מלא שורה ל-R2 ($0 egress, Class A 1M/month ראשון ליפול, blobs/uploads).
- מלא שורה ל-Durable Objects (strong consistency + coordination, GB-s ב-non-hibernating ראשון ליפול, WebSocket/locks).
- לכל אחד מ-3 התרחישים — sessions של login, רשימת משתמשים עם חיפוש, חדר צ'אט WebSocket — סמן את ה-primitive הנכון ונמק בשורה אחת.
פלט נראה לעין שתסיים איתו: טבלת החלטה מלאה עם 4 ה-primitives על פני 6 העמודות + 3 תרחישים משויכים עם נימוק (קובץ Markdown/Sheets). זו אחת משלוש ה-deliverables המרכזיות של הפרק.
- הוסף ל-
wrangler.tomlמפרק 1 את 4 הבלוקים:[[kv_namespaces]],[[d1_databases]],[[r2_buckets]],[[durable_objects.bindings]]+ בלוק[[migrations]]עםnew_sqlite_classes(לאnew_classes!). - הזן את ה-
id/database_id/bucket_nameשיצרת ב-do-now של כל סעיף. - כתוב נתיב Worker שכותב מפתח ל-KV וקורא אותו בחזרה (
env.MY_KV.put/get). - כתוב נתיב שמריץ
CREATE TABLE+INSERT+SELECTב-D1 עםWHERE(env.DB.prepare). - כתוב נתיב שמעלה אובייקט קטן ל-R2 וקורא אותו (
env.BUCKET.put/get) — שים לב להימנע מ-List. - כתוב מחלקת Durable Object מינימלית (
Room) עם counter ב-SQLite, ונתיב שמגדיל אותו דרך ה-binding. - הרץ
npx wrangler devואמת את 4 הנתיבים; ואזnpx wrangler dev --remoteכדי לראות אותם מול resources אמיתיים.
שלד מוכן להעתקה למחלקת ה-Room ולחיווט שלה מה-Worker (זה השלב הקשה — התאם, אל תכתוב מאפס):
// מחלקת ה-Durable Object — counter ב-SQLite דרך ctx.storage
export class Room {
constructor(ctx, env) {
this.ctx = ctx; // ctx.storage = ה-SQLite backend (new_sqlite_classes)
}
async fetch(request) {
let n = (await this.ctx.storage.get("count")) ?? 0;
n++;
await this.ctx.storage.put("count", n);
return Response.json({ count: n });
}
}
// ה-Worker מנתב /room ל-DO דרך ה-binding ROOM
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/room") {
const id = env.ROOM.idFromName("lobby"); // אותו שם → אותו instance
const stub = env.ROOM.get(id); // stub אל ה-DO
return stub.fetch(request); // מעבירים את הבקשה פנימה
}
// ...כאן יושבים נתיבי KV / D1 / R2 שכתבת בשלבים 3-5
return new Response("ok");
},
};
פלט נראה לעין שתסיים איתו: wrangler.toml אחד עם 4 bindings פעילים (KV + D1 + R2 + DO עם new_sqlite_classes) + Worker עם 4 נתיבים שכל אחד קורא/כותב ל-primitive שלו, מאומת ב-dev וב---remote. שמור צילום מסך של 4 התגובות ואת בלוק ה-wrangler.toml ב-README. זו פלטפורמת הזינוק לפרק 3.
מה קורה בקיר: למה ה-free tier נכשל סגור (ולא מחייב אותך בהפתעה)
זה אולי החלק הכי מרגיע בכל הפרק, ורוב המתחילים לא יודעים אותו: ב-free plan אתה לא מחויב על overage. כשאתה חוצה מגבלה יומית, פעולות מאותו סוג פשוט נכשלות עם שגיאה — עד reset ב-00:00 UTC. אין חשבון הפתעה.
בלשון התיעוד, זה עקבי בכל המוצרים. D1 FAQ: "כשהחשבון מגיע למגבלת ה-read ו/או write היומית, לא ניתן להריץ שאילתות מול D1." חציית storage = צריך למחוק נתונים לפני insert/create. DO: "אם תחרוג מאחת ממגבלות ה-free tier, פעולות נוספות מאותו סוג ייכשלו עם שגיאה." אותו ניסוח ל-KV ול-Hyperdrive. הקיר נסגר — הוא לא מחייב.
אז מה הסיפור על $4,868?
אולי שמעת על המפתח שקיבל חשבון של $4,868 כי באג infinite-loop כתב 3.45 מיליארד שורות ל-D1 ב-4 ימים. הסיפור אמיתי — אבל קורה רק ב-Paid. שם overage כתיבות D1 לא מוגבל, ואין hard billing cap (נכון למרץ 2026). ב-free plan אותו באג היה פשוט נעצר בקיר 100k ה-writes ביום הראשון, והפעולות היו נכשלות עד reset. אתה לא יכול לקבל חשבון כזה ב-$0.
הלקח למשדרגים: ברגע שאתה עובר ל-Paid, ה-"fail closed" הופך ל-"bill open". לפני שמדליקים Paid — הגדר wrangler CPU custom limits, ובנה write circuit breaker: מד שבודק את קצב הכתיבות (למשל כל ~30s) ומפיל מעגל אם הן קופצות חריג. ה-billing notifications של Cloudflare לא מתוכננים לתפוס באג שכותב מיליארד שורות ב-24 שעות.
איך circuit breaker כזה נראה בפועל, בלי להסתבך? רעיון פשוט: שמור ב-Durable Object או KV מונה כתיבות עם חלון זמן. לפני כל batch של כתיבות, בדוק כמה כתבת בחלון האחרון; אם עברת סף שהגדרת (נניח פי-3 מהקצב הצפוי), סרב להמשיך והתרע. זה לא מנגנון מתוחכם — זו רשת ביטחון שעוצרת לולאה אינסופית לפני שהיא הופכת ל-$4,868. ה-Vibe Coder לא צריך לבנות את זה ביום הראשון על free tier; אבל ביום שאתה לוחץ "Upgrade to Paid", זה הדבר הראשון שמוסיפים.
הסיבה שזה מספיק מרגיע: ב-free plan, סיפור האימה פשוט לא יכול לקרות. אותו באג infinite-loop היה נעצר בקיר 100k ה-writes ביום הראשון, הפעולות היו נכשלות עד 00:00 UTC, ואתה היית רואה את השגיאות ב-wrangler tail ומתקן. ה-free tier הוא, באופן פרדוקסלי, הסביבה הבטוחה ביותר ללמוד בה — כי המקסימום שאתה "מפסיד" הוא שהאפליקציה תפסיק לעבוד עד חצות, לא כסף.
בפועל (heuristic, לא דירוג רשמי של Cloudflare), זה הסדר הטיפוסי:
- 1. KV writes (1,000/day) — הקיר הראשון לכל אפליקציה עם state/analytics פר-משתמש. 1,000 משתמשים × write/יום = כבר במגבלה.
- 2. CPU 10ms — לכל compute לא-טריוויאלי (parse/regex/לולאות).
- 3. D1 rows-read (5M/day) — מתרוקן מ-full-scans/JOINs הרבה לפני 100k ה-writes.
- 4. DO duration (13,000 GB-s/day) — רק ל-DO שלא ב-hibernation.
- 5. Hyperdrive 100k queries/day ו-Queues 10k ops/day — קירות מאוחרים, לאפליקציות כבדות.
החלטה:
- חצית את הראשון מבין אלה ב-N הריאלי שלך? → $5 מוצדק (הקפיצה פרופורציונלית — למשל KV מ-1k ל-1M writes/חודש, CPU מ-10ms ל-30s).
- עוד מתחת לכולם? → הישאר ב-$0. אין סיבה לשלם.
הצג את הסדר הזה כ-"בפועל", לא כעובדה רשמית — האפליקציה שלך עשויה להפיל אותם בסדר אחר.
כתוב משפט אחד בקובץ המטריצה: "אני על ה-free plan, אז במקרה הגרוע פעולה תיכשל ב-00:00 UTC reset — לא אקבל חשבון של $4,868. זה קורה רק כשאעבור ל-Paid." זה ה-mindset שמרגיע אבל לא מרשל.
- פתח גיליון עם פרמטר קלט יחיד: N (מספר משתמשים פעילים ביום) ופעולות-לכל-משתמש לכל primitive.
- הזן נוסחה לכל primitive: KV writes = N × writes/user מול 1,000/day; D1 rows-written = N × ... מול 100k/day; D1 rows-read = N × scan-size מול 5M/day; DO GB-s מול 13,000/day; Queues ops מול 10k/day.
- סמן בצבע את העמודה הראשונה שחוצה את המגבלה החינמית עבור N נתון (התחל ב-N=1,000).
- כתוב מסקנה: "ב-N=___ המגבלה הראשונה שנופלת היא ___, ולכן צריך $5 כי ___".
- אמת את הסדר מול ה-heuristic מהסעיף "בקיר" (KV writes לרוב ראשון), וציין שזה heuristic ולא דירוג רשמי.
- הוסף שורה: "מה $5 פותח לאותו primitive" (למשל KV 1k→1M writes/חודש, CPU 10ms→30s).
פלט נראה לעין שתסיים איתו: מחשבון מגבלות (Sheets/Markdown עם נוסחאות) שמקבל N משתמשים ומסמן את המגבלה הראשונה שתיפול, עם משפט מסקנה pay-or-not מנומק + מה $5 פותח. שמור צילום מסך עם N=1,000. זו ה-deliverable השלישית של הפרק.
סיכום ומה הלאה: מ-Storage Primitives ל-AI ו-RAG
הפכת את ה-Worker ה"אמנזי" מפרק 1 ל-Worker עם זיכרון. ויותר חשוב — יש לך עכשיו מודל בחירה, לא ניחוש. בוא נסכם.
מה השגת
- טבלת החלטה KV / D1 / R2 / Durable Objects לפי consistency, מחיר ומגבלה ראשונה — עם 3 תרחישים משויכים.
wrangler.tomlעם 4 bindings פעילים + Worker עם 4 נתיבים שקוראים/כותבים לכל אחד, מאומת ב-devוב---remote.- מחשבון מגבלות שאומר לך איזו מגבלה תיפול ראשונה ב-N נתון, ומתי $5 מוצדק.
- מודל קיר ה-10ms: לזהות מיד אילו שורות שורפות CPU ואילו לא.
סדר נפילת המגבלות בפועל (heuristic)
אם תזכור דבר אחד מהפרק: ב-app stateful טיפוסי, KV writes (1k/day) נופל ראשון → ואז CPU (10ms) → ואז D1 rows-read מ-scans → ואז DO duration (רק לא-hibernating) → ואז Hyperdrive/Queues. זה heuristic, לא דירוג רשמי.
למה KV writes נופל ראשון כל-כך הרבה? כי 1,000 הוא מספר קטן בצורה מטעה. אפליקציה עם 1,000 משתמשים פעילים ביום, שכל אחד מבצע פעולה שכותבת ל-KV אחת ביום (עדכון session, ספירת ביקור, שמירת העדפה) — כבר במגבלה. וזה בדיוק הדפוס שמתחילים נופלים בו: הם מתייחסים ל-KV כמו ל-database כללי. ברגע שאתה מזהה שאתה כותב ל-KV פר-משתמש או פר-בקשה, אתה יודע שאתה על המסלול לקיר הזה — וזה הסימן לעבור ל-D1 (לנתונים) או DO (ל-state עקבי).
זה בדיוק מה שהמחשבון בתרגיל 4 הופך לקונקרטי: אתה מזין N (משתמשים/יום) ופעולות-לכל-משתמש, וה-נוסחה מראה לך בדיוק באיזה N כל מגבלה נשברת. במקום לנחש "אולי אצטרך לשלם", אתה רואה מספר: "ב-N=1,200 משתמשים, KV writes נשבר ראשון, ולכן $5". ההחלטה pay-or-not הופכת מחרדה למתמטיקה.
מילון 2026 — מה השתנה השנה
- Durable Objects SQLite — חיוב אחסון הופעל מתחילת ינואר 2026, וה-SQLite backend הוא היחיד החינמי.
- Queues הפכו חינמיים (10k ops/day) ב-2026-02-04.
- Workflows steps — ה-Paid הועלה ל-10,000 (עד 25,000) ב-2026-03-03; ה-free נשאר 1,024.
- KV cacheTtl — המינימום ירד מ-60s ל-30s ב-2026-01-30.
גשר לפרק 3
יש לך עכשיו 4 primitives פעילים — וזו בדיוק התשתית של פרק 3. ה-RAG chatbot על Workers AI שנבנה שם ישתמש מחדש ב-D1 ל-metadata של מסמכי ה-RAG, וב-KV ל-cache ו-rate-limiting. נוסיף לאותו wrangler.toml binding של AI ו-Vectorize, אבל ה-storage כבר עומד. הקיר ה-10ms שלמדת כאן יחזור גם שם — inference של AI הוא await (לא CPU), אבל עיבוד ה-embeddings וה-chunking כן.
| תדירות | פעולה |
|---|---|
| לפני כל בחירת storage | שאל את מסגרת ה-4 primitives: real-time/lock→DO, relational→D1, read-heavy קטן→KV, blob→R2. אל תברירת-מחדל ל-KV. |
| בכל שאילתת D1/DO | ודא שיש WHERE ו-index על עמודות הסינון. SELECT בלי WHERE על טבלה גדולה = rows-read שורף. |
| לפני deploy עם DO/WebSocket | ודא Hibernation API מופעל; smoke-test alarm/TTL/multipart עם wrangler dev --remote. |
| חודשי / לפני Paid | בדוק שימוש מול הקוטה (KV writes, CPU, D1 rows-read). לפני Paid — הגדר CPU custom limits + write circuit breaker. |
הוסף את 4 ה-bindings ל-wrangler.toml והרץ wrangler dev --remote עכשיו — אל תדחה את תרגיל 3. הרגע שבו ארבעת הנתיבים (KV/D1/R2/DO) מחזירים תשובה מול resources אמיתיים הוא מה שהופך את "ארבעה primitives" ממילון למיומנות. וזו בדיוק התשתית שפרק 3 בונה עליה — אתה מקים אותה פעם אחת.
שמור את ה-wrangler.toml עם 4 ה-bindings ואת מחשבון המגבלות ב-README של הפרויקט. בפרק 3 נוסיף binding של Workers AI ו-Vectorize לאותו קובץ ונשתמש ב-D1 ל-metadata של ה-RAG.
- מדוע
await fetch()באורך 500ms לא שורף את מגבלת ה-10ms CPU, אבלJSON.parseעל 50KB כן? (רמז: CPU time מודד execution, לא המתנה.) - למה counter שמתעדכן בכל request שייך ל-Durable Object ולא ל-KV — מה שני הקירות שהוא היה שובר ב-KV? (רמז: 1,000 writes/day + 1/שנייה לאותו key.)
- אתה מריץ
SELECT * FROM users WHERE email=?על טבלה של 50,000 שורות בלי index עלemail. כמה rows read זה עולה ולמה? (רמז: scan מול lookup.) - למה
ListObjectsב-R2 יכול לרוקן את המכסה מהר יותר מ-GetObject, למרות ש-List "רק קורא"? (רמז: Class A מול Class B.) - WebSocket hub שמחזיק 100 חיבורים idle שעה — מתי הוא שורף 460 GB-s ומתי הוא שורף כמעט אפס? (רמז: Hibernation API.)
בפרק הזה נתת ל-Worker זיכרון — ולמדת לבחור אותו נכון. ראית ש-"קיר ה-10ms" הוא קיר CPU, לא wall-clock: await על fetch/KV/D1 חינם, אבל JSON.parse/regex/לולאות שורפים. ומצד האחסון בנית מודל בחירה חד: real-time וקואורדינציה → Durable Objects (SQLite חינם, Hibernation למניעת GB-s), רלציוני → D1 (תמיד WHERE + index), read-heavy קטן → KV (1,000 writes/day, לא ל-state פר-בקשה), blob → R2 ($0 egress, הימנע מ-List).
ובעיקר — הקמת. יש לך wrangler.toml אחד עם 4 bindings פעילים, Worker שקורא/כותב לכל אחד, טבלת החלטה ומחשבון מגבלות אישי. והבנת את החדשות הטובות: ב-free plan אתה נכשל בקיר, לא מקבל חשבון — סיפור ה-$4,868 הוא תופעת Paid בלבד.
בפרק הבא ניקח את אותה תשתית בדיוק — אותו wrangler.toml, אותם D1 ו-KV — ונוסיף לה שכבת AI: RAG chatbot על Workers AI ו-Vectorize. ה-storage כבר עומד; עכשיו ניתן לו מוח.
צ'קליסט — סיכום פרק 2
- אני מבחין בין CPU time ל-wall-clock ויודע ש-
awaitעל fetch/KV/D1 לא נספר ב-10ms. - אני יודע לזהות מה שורף CPU (
JSON.parseגדול, regex כבד, לולאות, crypto) ומה לא. - אני יודע למה ה-CPU החינמי הוא 10ms ולא 50ms (ה-50ms הוא Bundled paid ישן).
- אני מבין את ה-50 subrequests/invocation ואת ה-cap הנפרד של 6 חיבורים בו-זמנית.
- בחרתי בין KV/D1/R2/DO ב-3 תרחישים ונימקתי לפי consistency, מחיר ומגבלה.
- אני יודע ש-KV מוגבל ל-1,000 writes/day (keys שונים) + 1/שנייה לאותו key, ולא ל-counter פר-בקשה.
- אני מבין ש-D1 rows-read סופר שורות שנסרקו, ושצריך תמיד
WHERE+ index. - אני יודע ש-
ListObjectsב-R2 הוא Class A (יקר) ושצריך לתכנן סביב GetObject. - אני יודע שה-SQLite backend של DO הוא היחיד החינמי, ומצהירים עליו עם
new_sqlite_classes. - אני מבין ש-DO ב-Hibernation לא צובר GB-s, ושזה הפתרון למלכודת ה-WebSocket hub.
- בניתי טבלת החלטה מלאה של 4 ה-primitives על 6 עמודות.
- הוספתי 4 bindings פעילים ל-
wrangler.toml(KV+D1+R2+DO) עם[[migrations]]תקין. - כתבתי Worker עם 4 נתיבים ואימתתי אותם ב-
wrangler devוב---remote. - בניתי מחשבון מגבלות שמקבל N ומסמן את המגבלה הראשונה שנופלת + מסקנת pay-or-not.
- אני מבין שב-free plan פעולות נכשלות בקיר (00:00 UTC reset) ושסיפור ה-$4,868 הוא Paid בלבד.
- שמרתי את ה-
wrangler.tomlואת המחשבון ב-README לקראת פרק 3.