2 שלב הבסיס

Workers ו-Storage — קיר ה-10ms, ובחירת KV / D1 / R2 / Durable Objects

בפרק 1 פרסת Worker שלא זוכר כלום. כאן ניתן לו זיכרון — ונפצח את שתי השאלות שכל אפליקציה stateful נופלת עליהן: כמה חישוב מותר לי ב-$0 (קיר ה-10ms), ואיפה בדיוק לשים את הנתונים (KV / D1 / R2 / Durable Objects) לפי consistency, מחיר, והמגבלה שתיפול ראשונה.

מה יהיה לך ביד בסוף הפרק
מטרות למידה
מה צריך לפני שמתחילים
חוט הפרויקט

הפרק הקודם (פרק 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.

מילון מונחים — פרק 2
מונחבעבריתהסבר
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 הלוקלי.
מתחיל6 דקותחינםמושג

למה הפרק הזה: מ-"Worker חי" ל-"Worker שזוכר דברים"

בסוף פרק 1 היה לך Worker חי על *.workers.dev — אבל הוא אמנזי. כל בקשה מתחילה מאפס; הוא לא זוכר מי נכנס, מה הזין, או מה קרה בבקשה הקודמת. ברגע שתרצה לבנות משהו אמיתי — login, רשימת משתמשים, העלאת קובץ, צ'אט בזמן אמת — אתה צריך state: מקום שבו נתונים שורדים בין בקשות.

הפרק הזה עונה על שתי שאלות שכל אפליקציה stateful נופלת עליהן, ושתיהן קובעות אם תישאר ב-$0 או תיאלץ לשלם:

הגישה כאן היא מודל-מנטלי-לפני-קוד. לפני שתכתוב 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.

עשו עכשיו 3 דקות

פתח את תיקיית הפרויקט מפרק 1 והרץ npx wrangler dev. ודא שה-Worker עדיין עולה ב-localhost:8787. זה ה-Worker שנוסיף לו storage bindings לאורך כל הפרק — אנחנו ממשיכים עליו, לא מתחילים מחדש.

בינוני9 דקותחינםמושג

קיר ה-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? כל חישוב מקומי סינכרוני:

הכלל המעשי: אם הקוד מחכה — הוא חינם. אם הקוד מחשב — הוא נספר. פצצת הזמן הקלאסית היא קוד שגודל ה-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, השדרוג מצדיק את עצמו.

הנה הויזואליזציה של ההבדל — אותה בקשה, שני סוגי זמן:

בקשה אחת: מה נספר ב-CPU ומה לא wall-clock (זמן שחלף) await fetch(...) ≈ 500ms — CPU פנוי, לא נספר parse 8ms CPU time (מה שנמדד מול הקיר) parse 8ms — ה-fetch לא תורם ל-CPU — קיר 10ms (חינם)
מסגרת החלטה: 10ms CPU — האם הקוד הזה ישרוף את התקציב?

שאל על כל קטע קוד:

החלטה כשזה שורף:

מקרה גבול: streaming/transform על stream נספר תוך כדי זרימה — מדוד אותו אמיתית עם wrangler tail, אל תנחש.

טעות נפוצה: להאמין שה-CPU החינמי הוא 50ms

למה זה מפתה: כמה בלוגים של 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/ לפני שאתה מסתמך על מספר.

עשו עכשיו 4 דקות

הוסף ל-Worker שורה: console.log('cpu test') ואז await fetch('https://example.com'). הרץ npx wrangler dev, פנה ל-Worker, וצפה בלוג. שים לב שה-await על fetch לא מקריס אותך — זמן הרשת הוא לא CPU time.

בינוני7 דקותחינםמושג

Subrequests: 50 חינם מול 10,000 בתשלום — ו-6 חיבורים במקביל

כל קריאת רשת יוצאת מ-Worker היא subrequest — וזה כולל לא רק fetch לשרתים חיצוניים, אלא גם כל פנייה ל-KV, D1, R2 או Workers AI. כל אלה נספרים מול אותה מכסה.

מגבלהFreePaid
Subrequests לכל invocation5010,000
חיבורים יוצאים בו-זמנית לכל request66

שים לב לעמודה השנייה: ה-cap של 6 חיבורים יוצאים בו-זמנית זהה בשני הטיירים. זה לא אותו דבר כמו ה-50 subrequests — אתה יכול לעשות 50 קריאות סך-הכל, אבל לא יותר מ-6 מהן פתוחות באותו רגע.

זה משנה איך אתה כותב fan-out. יש שני דפוסים:

הכלל: השתמש ב-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). הקופות הן הצוואר, לא מספר הלקוחות.

עשו עכשיו 4 דקות

בקוד ה-Worker, כתוב לולאה שמבצעת 3 fetch עם await בתוך הלולאה (סדרתי). לידה כתוב גרסה עם Promise.all של אותם 3 fetch (מקבילי). אל תריץ — רק סמן לעצמך מי מהן מהירה יותר ולמה (רמז: cap 6 חיבורים).

בינוני9 דקותחינםמושג

KV — ה-edge cache: eventually consistent, read-heavy, ומלכודת 1,000 הכתיבות

Workers KV הוא אחסון key-value מבוזר. הדבר היחיד שצריך להפנים עליו: הוא בנוי ל-קריאות, לא ל-כתיבות. אחרי קריאה ראשונה, הערך נשמר ב-edge cache קרוב למשתמש, וקריאות הבאות מהירות מאוד. אבל המודל שלו הוא eventually consistent — כתיבה מתפשטת גלובלית בהדרגה, וקריאה עלולה להחזיר ערך מעט ישן עד שה-cacheTtl פג.

המספרים החינמיים:

מגבלהFreeהערה
Reads/day100,000קריאה bulk נספרת כ-1
Writes/day1,000ל-keys שונים
Deletes/day1,000
List requests/day1,000
Storage1GBvalue עד 25MiB, key עד 512B
כתיבה לאותו key1/שנייה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 ל-counter פר-בקשה או ל-state עקבי

למה זה מפתה: "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 שנכתבים לעיתים רחוקות.

עשו עכשיו 3 דקות

הרץ npx wrangler kv namespace create MY_KV. העתק את ה-id שחזר. אל תוסיף עדיין ל-wrangler.toml — נעשה את זה בתרגיל ה-bindings. רק שמור את ה-id בצד.

בינוני9 דקותחינםמושג

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/day5,000,000
Rows written/day100,000
Storage כולל5GB
גודל DB500MB
מספר DBs10

וכאן המלכודת הגדולה — 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 על מה שאתה מחפש לפיו" הוא לא טיפ לביצועים בלבד — זה טיפ ל-תקציב. ב-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 כשמשהו איטי.

עשו עכשיו 4 דקות

הרץ npx wrangler d1 create my-app-db. שמור את ה-database_id שחזר. ואז npx wrangler d1 execute my-app-db --command "SELECT 1" כדי לוודא שהיא חיה ומגיבה.

בינוני8 דקותחינםמושג

R2 — אחסון blob ב-$0 egress: Class A מול Class B, ולמה List יקר

R2 הוא אחסון אובייקטים תואם-S3 — לקבצים גדולים: תמונות, וידאו, גיבויים, uploads של משתמשים. ה-killer feature שלו מול AWS S3 הוא $0 egress: אתה לא משלם על הוצאת נתונים. ב-S3 ה-egress הוא לעיתים העלות הגדולה ביותר; ב-R2 הוא אפס בכל הטיירים. בשביל מי שמגיש קבצים ללקוחות, זה חוסך הון.

המספרים החינמיים (Standard storage בלבד):

מגבלהFree
Storage10GB-month
Class A ops/month1,000,000
Class B ops/month10,000,000
Egressחינם ($0)

החיוב מתחלק לשני סוגי פעולות, ו-זו ההבחנה שתופסת אנשים:

ה-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"
עשו עכשיו 3 דקות

הרץ npx wrangler r2 bucket create my-app-bucket. ודא שהוא נוצר ברשימה עם npx wrangler r2 bucket list. שמור את שם ה-bucket לחיבור ב-wrangler.toml.

מתקדם10 דקותחינםמושג

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/day100,000 (HTTP + RPC + WebSocket messages + alarms)
Duration13,000 GB-s/day
SQLite rows read/day5,000,000
SQLite rows written/day100,000
Storage5GB (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 hub בלי Hibernation API — לשרוף 13,000 GB-s/day

למה זה מפתה: "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.

מסגרת החלטה: איזה storage primitive לבחור — KV / D1 / R2 / Durable Objects

התחל מ-consistency ולך מטה:

כלל אצבע: כתיבה כבדה או counter פר-בקשה → לא KV. full-text/relational → D1. real-time/lock → DO. bytes גדולים → R2.

הטבלה הויזואלית הזו מסכמת את ההחלטה לפי צבע consistency (eventually = כתום, strong = ירוק):

איזה primitive? התחל מ-consistency KV eventually consistent read-heavy, edge-cached config / flags sessions / secrets קיר ראשון: 1k writes/day D1 strongly consistent SQLite רלציוני רשומות משתמשים נתונים מקושרים קיר ראשון: 5M rows-read scans R2 eventually consistent blob, $0 egress תמונות / uploads גיבויים / קבצים קיר ראשון: Class A 1M/חודש Durable Objects strong + coordination real-time, single-thread WebSocket / presence נעילות / counter קיר ראשון: 13k GB-s (לא-hibernating)
עשו עכשיו 4 דקות

בלי לכתוב קוד DO עדיין — פתח https://developers.cloudflare.com/durable-objects/ וחפש את המונח "Hibernatable WebSocket". כתוב לעצמך משפט אחד: "DO ב-hibernation לא צובר GB-s כי ___".

בינוני8 דקותחינםרפרנס

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 לעבד עבודה כבדה בלי לשבור את הקיר.

טעות נפוצה: להניח ש-Queue retention שומר הודעות ללא הגבלה

למה זה מפתה: "התור יחזיק את ההודעות עד שה-consumer יקרא אותן" — נשמע כמו ההתנהגות הטבעית של תור.

למה זה טעות: ב-free tier ה-retention הוא 24 שעות לא-נשלט. אם ה-consumer שלך נפל ולא קם תוך יממה — ההודעות נמחקות, בלי אזהרה. בנוסף, כל retry וכל כתיבה ל-Dead Letter Queue נספרים כ-op נוסף מתוך ה-10,000 היומי.

מה לעשות במקום: תכנן את ה-consumer להיות עמיד (idempotent) ומנוטר. אם ההודעות קריטיות לטווח ארוך — שמור גם עותק ב-D1/R2, אל תסמוך על ה-retention של 24h. ל-retention ארוך יותר (עד 14 יום) צריך Paid.

עשו עכשיו 4 דקות

בטבלת המטריצה מפרק 1, הוסף 3 שורות: Workflows, Queues, Cron Triggers. רשום ליד כל אחת את המגבלה החינמית (1,024 steps / 10k ops-day / 5 triggers). נרחיב אותן לטבלת ההחלטה בתרגיל.

בינוני7 דקותחינםמושג

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 ומתי D1

Hyperdrive רלוונטי רק כשכבר יש לך Postgres/MySQL חיצוני שאתה לא רוצה להגר. לפרויקט חדש על Cloudflare — D1 הוא ברירת המחדל (מובנה, strongly consistent, אותו edge). Hyperdrive הוא גשר ל-DB קיים, לא תחליף ל-D1.

עשו עכשיו 2 דקות

אם יש לך DB חיצוני (Supabase/Neon) — רשום את ה-connection string במקום בטוח. אם אין, רק רשום לעצמך: "Hyperdrive רלוונטי רק כשיש Postgres/MySQL חיצוני; אחרת D1". לא יוצרים Hyperdrive בתרגיל זה.

בינוני8 דקותחינםhands-on

סודות ו-debug: wrangler secret put, wrangler tail, ומלכודות Miniflare מול --remote

ברגע שיש לך storage, יש לך גם סודות — מפתחות API, connection strings, tokens. יש שלוש דרכים לאחסן ערכי תצורה ב-Workers, ורק אחת נכונה לסוד production:

מנגנוןמה זהמתי
[vars] ב-wrangler.tomlplaintext, נכנס ל-gitרק ערכי תצורה לא-סודיים (שם סביבה, feature flag).
.dev.varsdotenv לוקלי, 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:

לכל אחד מאלה — smoke-test עם wrangler dev --remote, שרץ מול resources אמיתיים ב-edge במקום הסימולציה.

עשו עכשיו 3 דקות

הרץ npx wrangler secret put DEMO_SECRET והזן ערך כלשהו (למשל hello). זהו סוד production מוצפן — שונה מ-.dev.vars הלוקלי. הרץ npx wrangler secret list לאמת שהוא קיים.

תרגיל 1: מדידת קיר ה-10ms — מה שורף CPU ומה לא 15 דקות
  1. ב-Worker, כתוב handler שמודד Date.now() לפני ואחרי await fetch('https://example.com') ומחזיר את ההפרש ב-JSON (נתיב /fetch-test).
  2. הוסף נתיב שני (/parse-test): לולאה שמבצעת JSON.parse על מחרוזת גדולה (~50KB) או regex כבד, ומדוד אותו זמן.
  3. הרץ npx wrangler dev ופנה לשני הנתיבים; רשום את שני הזמנים.
  4. הסבר במשפט אחד למה ה-fetch (גם אם 500ms) לא מקריס את ה-10ms אבל ה-parse עלול.
  5. המר את נתיב ה-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 עוזר. שמור צילום מסך של שתי התגובות עם הזמנים.

תרגיל 2: טבלת החלטה KV / D1 / R2 / Durable Objects 20 דקות
  1. פתח טבלה (Markdown/Sheets) עם עמודות: primitive | consistency | פרופיל latency | מחיר/מגבלה חינמית | המגבלה שנופלת ראשונה | use-case מובהק.
  2. מלא שורה ל-KV (eventually consistent, edge-cached, 1k writes/day ראשון ליפול, config/sessions).
  3. מלא שורה ל-D1 (strongly consistent, single-primary, rows-read scans ראשון ליפול, נתונים רלציוניים).
  4. מלא שורה ל-R2 ($0 egress, Class A 1M/month ראשון ליפול, blobs/uploads).
  5. מלא שורה ל-Durable Objects (strong consistency + coordination, GB-s ב-non-hibernating ראשון ליפול, WebSocket/locks).
  6. לכל אחד מ-3 התרחישים — sessions של login, רשימת משתמשים עם חיפוש, חדר צ'אט WebSocket — סמן את ה-primitive הנכון ונמק בשורה אחת.

פלט נראה לעין שתסיים איתו: טבלת החלטה מלאה עם 4 ה-primitives על פני 6 העמודות + 3 תרחישים משויכים עם נימוק (קובץ Markdown/Sheets). זו אחת משלוש ה-deliverables המרכזיות של הפרק.

תרגיל 3: wrangler.toml עם 4 bindings פעילים + Worker שקורא/כותב לכל אחד 30 דקות
  1. הוסף ל-wrangler.toml מפרק 1 את 4 הבלוקים: [[kv_namespaces]], [[d1_databases]], [[r2_buckets]], [[durable_objects.bindings]] + בלוק [[migrations]] עם new_sqlite_classes (לא new_classes!).
  2. הזן את ה-id/database_id/bucket_name שיצרת ב-do-now של כל סעיף.
  3. כתוב נתיב Worker שכותב מפתח ל-KV וקורא אותו בחזרה (env.MY_KV.put / get).
  4. כתוב נתיב שמריץ CREATE TABLE + INSERT + SELECT ב-D1 עם WHERE (env.DB.prepare).
  5. כתוב נתיב שמעלה אובייקט קטן ל-R2 וקורא אותו (env.BUCKET.put / get) — שים לב להימנע מ-List.
  6. כתוב מחלקת Durable Object מינימלית (Room) עם counter ב-SQLite, ונתיב שמגדיל אותו דרך ה-binding.
  7. הרץ 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.

בינוני8 דקותfree מול paidאזהרה

מה קורה בקיר: למה ה-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 "בפועל", לא דירוג רשמי) 1. KV writes — 1,000/day (הקיר הראשון לרוב אפליקציה) 2. CPU 10ms — לכל compute לא-טריוויאלי 3. D1 rows-read — 5M/day (full-scans/JOINs) 4. DO duration — 13k GB-s (רק לא-hibernating) 5. Hyperdrive 100k / Queues 10k — קירות מאוחרים ראשון אחרון
מסגרת החלטה: איזו מגבלה תיפול ראשונה ומתי $5 מוצדק (heuristic "בפועל")

בפועל (heuristic, לא דירוג רשמי של Cloudflare), זה הסדר הטיפוסי:

החלטה:

הצג את הסדר הזה כ-"בפועל", לא כעובדה רשמית — האפליקציה שלך עשויה להפיל אותם בסדר אחר.

עשו עכשיו 3 דקות

כתוב משפט אחד בקובץ המטריצה: "אני על ה-free plan, אז במקרה הגרוע פעולה תיכשל ב-00:00 UTC reset — לא אקבל חשבון של $4,868. זה קורה רק כשאעבור ל-Paid." זה ה-mindset שמרגיע אבל לא מרשל.

תרגיל 4: מחשבון מגבלות — בהינתן N משתמשים, איזו מגבלה נופלת ראשונה 20 דקות
  1. פתח גיליון עם פרמטר קלט יחיד: N (מספר משתמשים פעילים ביום) ופעולות-לכל-משתמש לכל primitive.
  2. הזן נוסחה לכל 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.
  3. סמן בצבע את העמודה הראשונה שחוצה את המגבלה החינמית עבור N נתון (התחל ב-N=1,000).
  4. כתוב מסקנה: "ב-N=___ המגבלה הראשונה שנופלת היא ___, ולכן צריך $5 כי ___".
  5. אמת את הסדר מול ה-heuristic מהסעיף "בקיר" (KV writes לרוב ראשון), וציין שזה heuristic ולא דירוג רשמי.
  6. הוסף שורה: "מה $5 פותח לאותו primitive" (למשל KV 1k→1M writes/חודש, CPU 10ms→30s).

פלט נראה לעין שתסיים איתו: מחשבון מגבלות (Sheets/Markdown עם נוסחאות) שמקבל N משתמשים ומסמן את המגבלה הראשונה שתיפול, עם משפט מסקנה pay-or-not מנומק + מה $5 פותח. שמור צילום מסך עם N=1,000. זו ה-deliverable השלישית של הפרק.

מתחיל6 דקותחינםסיכום

סיכום ומה הלאה: מ-Storage Primitives ל-AI ו-RAG

הפכת את ה-Worker ה"אמנזי" מפרק 1 ל-Worker עם זיכרון. ויותר חשוב — יש לך עכשיו מודל בחירה, לא ניחוש. בוא נסכם.

מה השגת

סדר נפילת המגבלות בפועל (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 — מה השתנה השנה

גשר לפרק 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 כן.

שגרת עבודה — פרק 2
תדירותפעולה
לפני כל בחירת 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 בונה עליה — אתה מקים אותה פעם אחת.

עשו עכשיו 3 דקות

שמור את ה-wrangler.toml עם 4 ה-bindings ואת מחשבון המגבלות ב-README של הפרויקט. בפרק 3 נוסיף binding של Workers AI ו-Vectorize לאותו קובץ ונשתמש ב-D1 ל-metadata של ה-RAG.

בדוק את עצמך — 5 שאלות
  1. מדוע await fetch() באורך 500ms לא שורף את מגבלת ה-10ms CPU, אבל JSON.parse על 50KB כן? (רמז: CPU time מודד execution, לא המתנה.)
  2. למה counter שמתעדכן בכל request שייך ל-Durable Object ולא ל-KV — מה שני הקירות שהוא היה שובר ב-KV? (רמז: 1,000 writes/day + 1/שנייה לאותו key.)
  3. אתה מריץ SELECT * FROM users WHERE email=? על טבלה של 50,000 שורות בלי index על email. כמה rows read זה עולה ולמה? (רמז: scan מול lookup.)
  4. למה ListObjects ב-R2 יכול לרוקן את המכסה מהר יותר מ-GetObject, למרות ש-List "רק קורא"? (רמז: Class A מול Class B.)
  5. 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