Fastly Fiddle — Real-Edge VCL Testing
Trigger and scope
Trigger on: Fastly Fiddle, fiddle.fastly.dev URLs, the Fiddle HTTP API, CI testing of VCL services, real-edge VCL tests, shareable VCL reproductions, clientFetch.*/events.*/originFetches.* test expressions, .test.js / Mocha specs that target fiddles, SSE updateResult / waitingForSync events, remote VCL linting via Fiddle, or validating real Fastly behavior (geo, WAF, ESI, clustering, shielding) that local tools cannot simulate.
Do NOT use for: local VCL unit testing (use falco), fast TDD loops (Fiddle has a 10-20s edge-sync floor per publish), Fastly Compute/Wasm testing (use viceroy or fastlike), production service deployment (use fastly-cli), or anything requiring authenticated Fastly API access — Fiddle does not use your Fastly API key.
Fastly Fiddle is a web-based sandbox at https://fiddle.fastly.dev that compiles and runs VCL on real Fastly edge nodes. Because it uses the production VCL compiler and real POPs, it's the only way outside a real service to test VCL features that depend on edge infrastructure — geolocation data, WAF, ESI, clustering, shielding, rate limiting, real TLS, and real cache behavior.
Official UI: https://fiddle.fastly.dev
Demo CI runner: https://github.com/fastly/demo-fiddle-ci
API base: https://fiddle.fastly.dev (undocumented but stable; no auth required for public fiddles)
When Fiddle, when Falco
| Need |
Use |
| Fast local iteration (< 1s), watch mode, offline |
falco test |
| Real Fastly VCL compiler and semantics |
Fiddle |
Real client.geo.*, WAF, ESI, rate limiting, shield |
Fiddle |
| Shareable URL for bug repros and support tickets |
Fiddle |
| CI against real edge nodes |
Fiddle |
| Structured lint with line/col (no execution required) |
Either |
| Fastly Compute (WASM) |
Neither — use viceroy/fastlike |
Common workflow: iterate locally with falco test for speed, then push edge cases to Fiddle when you need real Fastly behavior or a shareable link. See falco-vs-fiddle.md for full trade-offs.
Workflow: deliverable first, then the cheapest check that answers your question
Two rules keep you out of the slow path, which is what wastes time and gets
agents killed by a wall-clock limit:
If your job is to produce a spec file, write it to disk first, before
any network call. The file is the deliverable — it must exist even if a
later publish stalls on a cold edge-sync. Don't build the spec only inside
a curl --data argument and lose it when the call blocks.
Match the check to the question. "Does this VCL compile / is the spec
shape accepted?" is answered by a single POST /fiddle reading valid —
~1-3s, no execution, no edge-sync wait (see gotcha #5). You only need the
full publish → execute → SSE round trip when you must observe runtime
assertion results (actual clientFetch/originFetches/events values),
and that pays the 10-120s edge-sync floor per publish. Executing is the
exception, not the default — reach for it deliberately, and always bound
your wait; never block indefinitely on the stream.
Lint-only (the common case): compile check, no execution
scripts/run-fiddle.sh --lint-only fiddle.json
# {fiddle_id, url, valid, lintStatus}. Exit 0 = compiles, 2 = lint error
# (details on stderr). No /execute, no SSE, no edge-sync wait.
The raw equivalent is one call — POST and read valid:
UA='fiddle-skill-example/1.0'
curl -sS --max-time 30 -X POST https://fiddle.fastly.dev/fiddle \
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
--data @fiddle.json | jq '{valid, lintStatus}' # valid==true ⇒ it compiles
Full round trip (only when you need runtime results)
When you genuinely need to see assertions pass/fail on the edge, the bundled
helper handles publish → execute → SSE → completion-detection in one call,
with a bounded --max-wait (default 180s per attempt) so it can't hang:
scripts/run-fiddle.sh examples/robots.json
# Prints fiddle URL, then pass/fail JSON per assertion (including body_preview
# and status by default). Exits non-zero on failure. Pass --no-bodies for
# compact output.
# Iterate against an already-published fiddle without paying edge-sync again:
scripts/run-fiddle.sh --id <fiddle-id> # re-execute (warmest, ~2s)
scripts/run-fiddle.sh --id <fiddle-id> spec.json # PUT then execute
The equivalent raw-curl flow, for reference or when the helper isn't available:
UA='fiddle-skill-example/1.0'
# 1. Create. Capture the ID and the validity flag.
RESP=$(curl -sS -X POST https://fiddle.fastly.dev/fiddle \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-H "User-Agent: $UA" \
--data '{
"origins": ["https://http-me.fastly.dev"],
"vcl": {
"init": "# Synthetic robots.txt response\n# via error restart pattern",
"recv": "if (req.url.path == \"/robots.txt\") {\n error 601;\n}",
"error": "if (obj.status == 601) {\n set obj.status = 200;\n synthetic {\"User-agent: BadBot\"};\n return(deliver);\n}"
},
"requests": [
{ "path": "/robots.txt",
"headers": "X-Custom: value\nX-Other: second",
"tests": ["clientFetch.status is 200", "clientFetch.bodyPreview includes \"BadBot\""] }
]
}')
FID=$(echo "$RESP" | jq -r '.fiddle.id')
echo "$RESP" | jq '{valid, lintStatus}' # bail here if valid is false
# 2. Execute. Subscribe to the SSE stream IMMEDIATELY — session IDs expire fast.
SID=$(curl -sS -X POST "https://fiddle.fastly.dev/fiddle/$FID/execute?cacheID=1" \
-H 'Accept: application/json' -H "User-Agent: $UA" | jq -r '.sessionID')
# 3. Stream results. Emits repeated `event: waitingForSync` (~10-20s on first
# publish) then `event: updateResult` with full pass/fail data. ALWAYS cap
# the stream with --max-time so a slow/cold sync can't block you forever;
# on timeout, re-execute the same $FID (warm, ~2s) rather than re-publishing.
curl -sS -N --max-time 120 -H "User-Agent: $UA" "https://fiddle.fastly.dev/results/$SID/stream"
Full protocol details in api.md.
Wire-format gotchas
Non-obvious behavior that will break tools round-tripping fiddles programmatically:
vcl on input, src on output. You POST {"vcl": {"recv": "..."}} but GET returns {"src": {"recv": "..."}}. The server renames the key on normalization. Any tool that fetches a fiddle and re-publishes it must map src → vcl (or send src — both work on input). Update existing fiddles with PUT /fiddle/:id — same body shape as POST, but partial updates are not supported; omitted subroutines are cleared.
tests is a string on the wire. You can send tests: ["a", "b"] but a subsequent GET returns tests: "a\nb". One assertion per line. Split on \n when reading.
headers is a newline-joined string, not an array. Unlike tests (which accepts both), headers must be a string: "headers": "User-Agent: BadBot/1.0\nX-Custom: value". An array will be rejected with a validation error.
Request fields are auto-defaulted by the server. A GET of a fiddle you just created will include fields you didn't send: method: "GET", connType: "h2" (HTTP/2 by default — matters for tests that depend on protocol), enableCluster: true, enableShield: false, useFreshCache: false, sourceIP: "client", followRedirects: false, delay: 0. Set them explicitly if you care.
Invalid VCL still gets a fiddle ID. POST returns {valid: false, lintStatus: {...}, fiddle: {id, ...}} for broken VCL. On a create/update (POST/PUT) response, valid is the lint result — true if the VCL compiles, false if not, with details in lintStatus. That's the number to trust for "does this compile?", and you get it without executing anything. Don't rely on HTTP status.
But valid means something different on a GET. There it tracks execution, not compilation: it stays false until the fiddle has been executed at least once, then flips to true. A fiddle that lints perfectly cleanly still reads back as valid: false right after you create it. So judge compilation from the create/update response (or by re-submitting the spec) — never from a GET. A GET can't distinguish a valid-but-not-yet-run fiddle from a genuinely broken one: both come back valid: false with an empty lintStatus.
VCL string concat with + rejects parenthesized operands. set X = "used=" + (a - b); fails with a misleading "Remove the trailing + operator" suggestion — the + is fine, the ( is what the parser rejects. Compute the sub-expression into a local variable first. See spec-shape.md.
error 8NN; / error 9NN; is rejected by Fiddle lint — use 6xx. Any 800–999 code fails with "8xx and 9xx error codes are used internally by Fastly. Use 6xx instead.", in any subroutine and any context (bare or inside if/else/switch). So the classic error 801 <url>; redirect idiom trips Fiddle lint even inside an if — use error 602 "<url>"; and build the 301 in vcl_error. Codes 400–799 are accepted. See spec-shape.md.
Some test expressions have built-in delays. originFetches.count() is 0 returns asyncDelay: 2500 — the server waits 2.5s before evaluating "did nothing happen?". Client wait time must accommodate this; 45-60s is a safe ceiling.
SSE session IDs are short-lived. Subscribe to /results/<sessionID>/stream within seconds of receiving the ID from /execute. Delayed connections get a 404. Always have the stream open before you start waiting on results.
Test DSL has unusual syntax. No .first(), no reqHeaderValue(), no isnt/empty operators. Event objects expose only url/method/return/status/ttl — not arbitrary req.http.* values. Read test-dsl.md before writing assertions.
The server assigns a fresh fiddle ID on every POST /fiddle — even for byte-identical input. IDs are not a content hash; back-to-back POSTs of the same body return different IDs, and each new ID needs its own edge-sync pass before /execute can produce results. PUT /fiddle/<id> keeps the URL stable (good for shared bug-repro links) but the new VCL still recompiles and propagates, so PUTs pay the same edge-sync cost as POSTs. The genuinely warm path is re-executing an unchanged fiddle ID — same content, repeat /execute calls finish in ~2s. Capture the ID from the first POST and reuse it; vary cacheID to force cold caches without re-publishing. (PUT is not partial — omitted subroutines are cleared, see #1.)
originFetches.count() is N is fragile under retries and shared cacheID. A retry — automatic in run-fiddle.sh, or manual via --id <fid> — re-executes against the same cacheID, so any origin response cached on the previous attempt is now a HIT and originFetches.count() drops to 0. Two reliable fixes: set useFreshCache: true on the request (forces a fresh cache, ignoring the session cacheID — see spec-shape.md Request objects), or assert via events.where(fnName=fetch).count() (counts subroutine entries, not network calls). The same goes for originFetches[0].* assertions whenever the test runs after a possible warmup.
Authoring conventions
Fiddles are read by humans in a browser. These aren't surprises, but they make shared fiddles useful:
- Set a
title. Makes fiddles findable in browser tabs, bookmarks, and shared links. Example: "title": "fastly_info.state: compound values deep dive".
- Use
init as a header comment. The init subroutine renders first in the UI. Put a short summary there (~55 chars/line): "init": "# fastly_info.state: compound values\n# Demonstrates MISS-CLUSTER, HIT-CLUSTER, HIT-SYNTH".
- Format VCL with
\n and indentation rather than cramming everything onto one line.
- Send
User-Agent: <tool>/<version> on every API call. Fiddle is unauthenticated shared infra; default curl/x.y or library UAs are bad citizenship. This is the API call's UA, not the simulated request's headers.
Testing in CI
Reference implementation: fastly/demo-fiddle-ci — a Node + Mocha harness. Clone it and write your {spec, scenarios[]}.
The one non-obvious thing: it publishes the fiddle once, then re-executes the same fiddle ID per scenario with different requests[]. Only re-execution is warm (~2s); a fresh publish pays the 10-20s edge-sync floor (see "Limits" below and gotcha #11). Keep scenarios sequential.
References
| Topic |
File |
Use when... |
| Helper script |
scripts/run-fiddle.sh |
Publishing + executing + streaming a fiddle in one shell command |
| Example payload |
examples/robots.json |
Starting from a known-good minimal fiddle spec |
| HTTP API |
api.md |
Calling Fiddle endpoints directly, driving it from any language |
| Fiddle spec shape |
spec-shape.md |
Building the JSON payload: origins, src, requests, defaults |
| Test DSL |
test-dsl.md |
Writing clientFetch.*, events.where(...), originFetches.* |
| Falco vs Fiddle |
falco-vs-fiddle.md |
Choosing the right tool, or combining them in one workflow |
Limits and cautions
- No auth required, no quota documented. Be a good citizen: don't hammer the API in tight loops, and always send a descriptive
User-Agent on API calls (see Authoring conventions). Use cacheID consistently across requests that need to share cache, and vary it to force cold caches.
- Edge-sync floor is ~10-20s per publish, sometimes much longer. Applies per fiddle ID, not per unique content — and every
POST /fiddle mints a new ID (see Wire-format gotchas #11), so re-publishing identical input still pays the full sync cost. Cold publishes regularly take 60-120s in practice. Unusable for TDD. Batch changes; execute once per meaningful delta; reuse IDs via PUT when iterating.
- Execution hops through a real POP (tests observed running from IAD on node
kiad7000140). Geographic assertions reflect wherever the fiddle executor landed.
- Fiddles are public by default. Don't put secrets in VCL you publish.
- The API is undocumented. Field names and behavior can change.
1---2name: fastly-fiddle3description: Use when testing VCL against real Fastly edge infrastructure, writing assertion-based Fiddle tests, producing shareable fiddle URLs for bug reproductions, running VCL integration tests in CI, linting VCL remotely via the Fiddle API, or working with clientFetch/events/originFetches test expressions.4---56# Fastly Fiddle — Real-Edge VCL Testing78## Trigger and scope910Trigger on: Fastly Fiddle, fiddle.fastly.dev URLs, the Fiddle HTTP API, CI testing of VCL services, real-edge VCL tests, shareable VCL reproductions, `clientFetch.*`/`events.*`/`originFetches.*` test expressions, `.test.js` / Mocha specs that target fiddles, SSE `updateResult` / `waitingForSync` events, remote VCL linting via Fiddle, or validating real Fastly behavior (geo, WAF, ESI, clustering, shielding) that local tools cannot simulate.1112Do NOT use for: local VCL unit testing (use `falco`), fast TDD loops (Fiddle has a 10-20s edge-sync floor per publish), Fastly Compute/Wasm testing (use `viceroy` or `fastlike`), production service deployment (use `fastly-cli`), or anything requiring authenticated Fastly API access — Fiddle does not use your Fastly API key.1314Fastly Fiddle is a web-based sandbox at <https://fiddle.fastly.dev> that compiles and runs VCL on real Fastly edge nodes. Because it uses the production VCL compiler and real POPs, it's the only way outside a real service to test VCL features that depend on edge infrastructure — geolocation data, WAF, ESI, clustering, shielding, rate limiting, real TLS, and real cache behavior.1516**Official UI**: <https://fiddle.fastly.dev>17**Demo CI runner**: <https://github.com/fastly/demo-fiddle-ci>18**API base**: `https://fiddle.fastly.dev` (undocumented but stable; no auth required for public fiddles)1920## When Fiddle, when Falco2122| Need | Use |23| ----------------------------------------------------- | ---------------------------------- |24| Fast local iteration (< 1s), watch mode, offline | `falco test` |25| Real Fastly VCL compiler and semantics | Fiddle |26| Real `client.geo.*`, WAF, ESI, rate limiting, shield | Fiddle |27| Shareable URL for bug repros and support tickets | Fiddle |28| CI against real edge nodes | Fiddle |29| Structured lint with line/col (no execution required) | Either |30| Fastly Compute (WASM) | Neither — use `viceroy`/`fastlike` |3132Common workflow: iterate locally with `falco test` for speed, then push edge cases to Fiddle when you need real Fastly behavior or a shareable link. See [falco-vs-fiddle.md](references/falco-vs-fiddle.md) for full trade-offs.3334## Workflow: deliverable first, then the cheapest check that answers your question3536Two rules keep you out of the slow path, which is what wastes time and gets37agents killed by a wall-clock limit:38391. **If your job is to produce a spec file, write it to disk first**, before40 any network call. The file is the deliverable — it must exist even if a41 later publish stalls on a cold edge-sync. Don't build the spec only inside42 a `curl --data` argument and lose it when the call blocks.43442. **Match the check to the question.** "Does this VCL compile / is the spec45 shape accepted?" is answered by a _single_ `POST /fiddle` reading `valid` —46 ~1-3s, no execution, no edge-sync wait (see gotcha #5). You only need the47 full publish → execute → SSE round trip when you must observe _runtime_48 assertion results (actual `clientFetch`/`originFetches`/`events` values),49 and that pays the 10-120s edge-sync floor per publish. **Executing is the50 exception, not the default** — reach for it deliberately, and always bound51 your wait; never block indefinitely on the stream.5253### Lint-only (the common case): compile check, no execution5455```bash56scripts/run-fiddle.sh --lint-only fiddle.json57# {fiddle_id, url, valid, lintStatus}. Exit 0 = compiles, 2 = lint error58# (details on stderr). No /execute, no SSE, no edge-sync wait.59```6061The raw equivalent is one call — POST and read `valid`:6263```bash64UA='fiddle-skill-example/1.0'65curl -sS --max-time 30 -X POST https://fiddle.fastly.dev/fiddle \66 -H 'Content-Type: application/json' -H "User-Agent: $UA" \67 --data @fiddle.json | jq '{valid, lintStatus}' # valid==true ⇒ it compiles68```6970### Full round trip (only when you need runtime results)7172When you genuinely need to see assertions pass/fail on the edge, the bundled73helper handles publish → execute → SSE → completion-detection in one call,74with a bounded `--max-wait` (default 180s per attempt) so it can't hang:7576```bash77scripts/run-fiddle.sh examples/robots.json78# Prints fiddle URL, then pass/fail JSON per assertion (including body_preview79# and status by default). Exits non-zero on failure. Pass --no-bodies for80# compact output.8182# Iterate against an already-published fiddle without paying edge-sync again:83scripts/run-fiddle.sh --id <fiddle-id> # re-execute (warmest, ~2s)84scripts/run-fiddle.sh --id <fiddle-id> spec.json # PUT then execute85```8687The equivalent raw-curl flow, for reference or when the helper isn't available:8889```bash90UA='fiddle-skill-example/1.0'9192# 1. Create. Capture the ID and the validity flag.93RESP=$(curl -sS -X POST https://fiddle.fastly.dev/fiddle \94 -H 'Content-Type: application/json' -H 'Accept: application/json' \95 -H "User-Agent: $UA" \96 --data '{97 "origins": ["https://http-me.fastly.dev"],98 "vcl": {99 "init": "# Synthetic robots.txt response\n# via error restart pattern",100 "recv": "if (req.url.path == \"/robots.txt\") {\n error 601;\n}",101 "error": "if (obj.status == 601) {\n set obj.status = 200;\n synthetic {\"User-agent: BadBot\"};\n return(deliver);\n}"102 },103 "requests": [104 { "path": "/robots.txt",105 "headers": "X-Custom: value\nX-Other: second",106 "tests": ["clientFetch.status is 200", "clientFetch.bodyPreview includes \"BadBot\""] }107 ]108 }')109FID=$(echo "$RESP" | jq -r '.fiddle.id')110echo "$RESP" | jq '{valid, lintStatus}' # bail here if valid is false111112# 2. Execute. Subscribe to the SSE stream IMMEDIATELY — session IDs expire fast.113SID=$(curl -sS -X POST "https://fiddle.fastly.dev/fiddle/$FID/execute?cacheID=1" \114 -H 'Accept: application/json' -H "User-Agent: $UA" | jq -r '.sessionID')115116# 3. Stream results. Emits repeated `event: waitingForSync` (~10-20s on first117# publish) then `event: updateResult` with full pass/fail data. ALWAYS cap118# the stream with --max-time so a slow/cold sync can't block you forever;119# on timeout, re-execute the same $FID (warm, ~2s) rather than re-publishing.120curl -sS -N --max-time 120 -H "User-Agent: $UA" "https://fiddle.fastly.dev/results/$SID/stream"121```122123Full protocol details in [api.md](references/api.md).124125## Wire-format gotchas126127Non-obvious behavior that will break tools round-tripping fiddles programmatically:1281291. **`vcl` on input, `src` on output.** You `POST {"vcl": {"recv": "..."}}` but `GET` returns `{"src": {"recv": "..."}}`. The server renames the key on normalization. Any tool that fetches a fiddle and re-publishes it must map `src` → `vcl` (or send `src` — both work on input). **Update existing fiddles with `PUT /fiddle/:id`** — same body shape as `POST`, but partial updates are not supported; omitted subroutines are cleared.1301312. **`tests` is a string on the wire.** You can send `tests: ["a", "b"]` but a subsequent `GET` returns `tests: "a\nb"`. One assertion per line. Split on `\n` when reading.1321333. **`headers` is a newline-joined string, not an array.** Unlike `tests` (which accepts both), `headers` must be a string: `"headers": "User-Agent: BadBot/1.0\nX-Custom: value"`. An array will be rejected with a validation error.1341354. **Request fields are auto-defaulted by the server.** A `GET` of a fiddle you just created will include fields you didn't send: `method: "GET"`, `connType: "h2"` (HTTP/2 by default — matters for tests that depend on protocol), `enableCluster: true`, `enableShield: false`, `useFreshCache: false`, `sourceIP: "client"`, `followRedirects: false`, `delay: 0`. Set them explicitly if you care.1361375. **Invalid VCL still gets a fiddle ID.** `POST` returns `{valid: false, lintStatus: {...}, fiddle: {id, ...}}` for broken VCL. On a **create/update** (`POST`/`PUT`) response, `valid` is the lint result — `true` if the VCL compiles, `false` if not, with details in `lintStatus`. That's the number to trust for "does this compile?", and you get it without executing anything. Don't rely on HTTP status.138139 **But `valid` means something different on a `GET`.** There it tracks execution, not compilation: it stays `false` until the fiddle has been executed at least once, then flips to `true`. A fiddle that lints perfectly cleanly still reads back as `valid: false` right after you create it. So judge compilation from the create/update response (or by re-submitting the spec) — never from a `GET`. A `GET` can't distinguish a valid-but-not-yet-run fiddle from a genuinely broken one: both come back `valid: false` with an empty `lintStatus`.1401416. **VCL string concat with `+` rejects parenthesized operands.** `set X = "used=" + (a - b);` fails with a _misleading_ "Remove the trailing `+` operator" suggestion — the `+` is fine, the `(` is what the parser rejects. Compute the sub-expression into a local variable first. See [spec-shape.md](references/spec-shape.md#vcl-string-concatenation).1421437. **`error 8NN;` / `error 9NN;` is rejected by Fiddle lint — use 6xx.** Any 800–999 code fails with "8xx and 9xx error codes are used internally by Fastly. Use 6xx instead.", in any subroutine and any context (bare or inside `if`/`else`/`switch`). So the classic `error 801 <url>;` redirect idiom trips Fiddle lint even inside an `if` — use `error 602 "<url>";` and build the 301 in `vcl_error`. Codes 400–799 are accepted. See [spec-shape.md](references/spec-shape.md#error-codes-use-6xx-for-synthetics).1441458. **Some test expressions have built-in delays.** `originFetches.count() is 0` returns `asyncDelay: 2500` — the server waits 2.5s before evaluating "did nothing happen?". Client wait time must accommodate this; 45-60s is a safe ceiling.1461479. **SSE session IDs are short-lived.** Subscribe to `/results/<sessionID>/stream` within seconds of receiving the ID from `/execute`. Delayed connections get a 404. Always have the stream open before you start waiting on results.14814910. **Test DSL has unusual syntax.** No `.first()`, no `reqHeaderValue()`, no `isnt`/`empty` operators. Event objects expose only `url`/`method`/`return`/`status`/`ttl` — not arbitrary `req.http.*` values. **Read [test-dsl.md](references/test-dsl.md) before writing assertions.**15015111. **The server assigns a fresh fiddle ID on every `POST /fiddle`** — even for byte-identical input. IDs are not a content hash; back-to-back POSTs of the same body return different IDs, and each new ID needs its own edge-sync pass before `/execute` can produce results. **`PUT /fiddle/<id>`** keeps the URL stable (good for shared bug-repro links) but the new VCL still recompiles and propagates, so PUTs pay the same edge-sync cost as POSTs. The genuinely warm path is **re-executing an unchanged fiddle ID** — same content, repeat `/execute` calls finish in ~2s. Capture the ID from the first POST and reuse it; vary `cacheID` to force cold caches without re-publishing. (PUT is not partial — omitted subroutines are cleared, see #1.)15215312. **`originFetches.count() is N` is fragile under retries and shared `cacheID`.** A retry — automatic in `run-fiddle.sh`, or manual via `--id <fid>` — re-executes against the same `cacheID`, so any origin response cached on the previous attempt is now a HIT and `originFetches.count()` drops to 0. Two reliable fixes: set **`useFreshCache: true`** on the request (forces a fresh cache, ignoring the session `cacheID` — see [spec-shape.md `Request objects`](references/spec-shape.md#request-objects)), or assert via **`events.where(fnName=fetch).count()`** (counts subroutine entries, not network calls). The same goes for `originFetches[0].*` assertions whenever the test runs after a possible warmup.154155## Authoring conventions156157Fiddles are read by humans in a browser. These aren't surprises, but they make shared fiddles useful:158159- **Set a `title`.** Makes fiddles findable in browser tabs, bookmarks, and shared links. Example: `"title": "fastly_info.state: compound values deep dive"`.160- **Use `init` as a header comment.** The `init` subroutine renders first in the UI. Put a short summary there (~55 chars/line): `"init": "# fastly_info.state: compound values\n# Demonstrates MISS-CLUSTER, HIT-CLUSTER, HIT-SYNTH"`.161- **Format VCL with `\n` and indentation** rather than cramming everything onto one line.162- **Send `User-Agent: <tool>/<version>` on every API call.** Fiddle is unauthenticated shared infra; default `curl/x.y` or library UAs are bad citizenship. This is the API call's UA, not the simulated request's `headers`.163164## Testing in CI165166Reference implementation: [fastly/demo-fiddle-ci](https://github.com/fastly/demo-fiddle-ci) — a Node + Mocha harness. Clone it and write your `{spec, scenarios[]}`.167168The one non-obvious thing: it publishes the fiddle once, then re-executes the same fiddle ID per scenario with different `requests[]`. Only re-execution is warm (~2s); a fresh publish pays the 10-20s edge-sync floor (see "Limits" below and gotcha #11). Keep scenarios sequential.169170## References171172| Topic | File | Use when... |173| ------------------- | --------------------------------------------------- | ------------------------------------------------------------------ |174| **Helper script** | [scripts/run-fiddle.sh](scripts/run-fiddle.sh) | Publishing + executing + streaming a fiddle in one shell command |175| **Example payload** | [examples/robots.json](examples/robots.json) | Starting from a known-good minimal fiddle spec |176| **HTTP API** | [api.md](references/api.md) | Calling Fiddle endpoints directly, driving it from any language |177| Fiddle spec shape | [spec-shape.md](references/spec-shape.md) | Building the JSON payload: origins, src, requests, defaults |178| Test DSL | [test-dsl.md](references/test-dsl.md) | Writing `clientFetch.*`, `events.where(...)`, `originFetches.*` |179| Falco vs Fiddle | [falco-vs-fiddle.md](references/falco-vs-fiddle.md) | Choosing the right tool, or combining them in one workflow |180181## Limits and cautions182183- **No auth required, no quota documented.** Be a good citizen: don't hammer the API in tight loops, and always send a descriptive `User-Agent` on API calls (see Authoring conventions). Use `cacheID` consistently across requests that need to share cache, and vary it to force cold caches.184- **Edge-sync floor is ~10-20s per publish, sometimes much longer.** Applies per fiddle ID, not per unique content — and every `POST /fiddle` mints a new ID (see Wire-format gotchas #11), so re-publishing identical input still pays the full sync cost. Cold publishes regularly take 60-120s in practice. Unusable for TDD. Batch changes; execute once per meaningful delta; reuse IDs via `PUT` when iterating.185- **Execution hops through a real POP** (tests observed running from IAD on node `kiad7000140`). Geographic assertions reflect wherever the fiddle executor landed.186- **Fiddles are public by default.** Don't put secrets in VCL you publish.187- **The API is undocumented.** Field names and behavior can change.