The Free Dictionary API is one person's gift to the internet โ€” and your CI shouldn't be part of its 10 million requests a month

dictionaryapi.dev is the API behind nearly every "build a dictionary app" tutorial: keyless, free, open-source (meetDeveloper/freeDictionaryAPI), with Wiktionary-sourced definitions, phonetics, and pronunciation audio. It's run by a single maintainer on donations, and by the README's own account it serves more than 10 million requests a month. That's the context for what we measured while writing this guide (September 2026):

$ curl --max-time 15 "https://api.dictionaryapi.dev/api/v2/entries/en/hello"
curl: (28) Operation timed out after 15001 milliseconds with 0 bytes received

# with a bigger budget it does answer โ€” in twenty seconds:
$ curl -w " (%{http_code} in %{time_total}s)" ".../api/v2/entries/en/hello"
[{"word":"hello", โ€ฆ }]  (200 in 19.96s)

# and words the CDN hasn't cached don't answer at all:
$ curl -w " (%{http_code} in %{time_total}s)" ".../api/v2/entries/en/coffee"
error code: 522  (522 in 20.13s)

In our timed runs, every call took ~20 seconds. Words with a stale Cloudflare cache copy (cf-cache-status: STALE โ€” the hello we got had an age of 53 days) eventually returned 200; everything else โ€” including world and coffee โ€” came back as HTTP 522 with the plain-text body error code: 522. Note what that does to tutorial code: res.json() throws on that body, and the friendly documented 404 ("title":"No Definitions Found"โ€ฆ) never gets a chance to appear. There's also a real rate limit โ€” the API's own headers advertise x-ratelimit-limit: 450, which matches the documented 450 requests per 5 minutes per IP.

None of this is a complaint. A free, keyless dictionary API maintained by one person is a genuinely kind thing to exist, and light production use (with caching) plus a donation is how it stays alive. But a dev server re-firing lookups on every keystroke, or a CI matrix querying it in parallel, is fragile for you and load for them. Point those at a mock that answers instantly with the exact shapes.

1. Serve captured entries on the same URLs, byte-for-byte

Capture the words your app and tests actually use (be patient โ€” give it a generous timeout), then reproduce the exact paths as custom routes. One curl creates a project, no signup:

curl -s --max-time 60 "https://api.dictionaryapi.dev/api/v2/entries/en/hello" > hello.json
curl -s --max-time 60 "https://api.dictionaryapi.dev/api/v2/entries/en/serendipity" > serendipity.json

curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects?name=dictionary-mock" | tee project.json
# โ†’ {"id":"YOUR_ID","adminKey":"YOUR_ADMIN_KEY",โ€ฆ}

jq '{path:"/api/v2/entries/en/hello", body:.}' hello.json \
  | curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
    -H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' --data-binary @-
jq '{path:"/api/v2/entries/en/serendipity", body:.}' serendipity.json \
  | curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
    -H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' --data-binary @-

Now GET /m/YOUR_ID/api/v2/entries/en/hello answers instantly with the array-of-entries shape your renderer expects โ€” word, phonetics (with audio URLs), meanings[].partOfSpeech, definitions[].definition, synonyms, antonyms, license, sourceUrls โ€” because it is the real response, replayed. We verified both served routes parse-equal to the live captures. Swap the base URL in your app config and nothing else changes.

2. The documented 404 your error path has never actually seen

When the API is healthy, a missing word answers a friendly JSON 404. Right now (see above) misses answer a plain-text 522 instead โ€” so your "no definitions" UI state may literally never have been exercised against the real shape. Make it a permanent fixture:

curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
  -H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
  -d '{"path":"/api/v2/entries/en/xyzzy","status":404,"body":{"title":"No Definitions Found","message":"Sorry pal, we could not find definitions for the word you were looking for.","resolution":"You can try the search again at later time or head to the web instead."}}'

One test looks up hello and renders definitions; another looks up xyzzy and asserts the empty-state UI. Both deterministic, both instant.

3. Order today's failure modes on demand

4. Prefer a browsable dataset? Import the entries as records

If you'd rather query than replay URLs, host the same captures as a resource โ€” records keep every nested field verbatim:

jq -s '{words: [.[][0]]}' hello.json serendipity.json > db.json
curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=dictionary-data" \
  -H 'content-type: application/json' --data-binary @db.json

The import answers with honest warnings (nested arrays like meanings are kept verbatim in the records but skipped from the flat field schema), and you get the full query toolkit: ?word=serendipity exact-matches, ?q=greeting full-text-searches inside the nested definitions, ?select=word projects, writes persist, and /m/YOUR_ID/types.ts generates TypeScript interfaces from your records (?format=zod for Zod).

5. Honest comparison

dictionaryapi.devYour mock
Real dictionary breadthโœ” any English word, phonetics, audioโœ˜ only the words you capture โ€” by design
Signup / keynonenone
Latency~20s per call in our Sep 2026 runsedge-served, instant
Reliabilityuncached words answered 522 in our runsindependent of their origin
Rate limit450 req / 5 min / IP10,000/day per project
Deterministic for testsโœ˜โœ”
Can simulate 522 / stall / 429 / 404โœ˜ (only by accident)โœ” routes + mock_delay/mock_seq/mock_ratelimit
Right forlight production use, cached โ€” and donatedev, CI, demos, workshops

The pattern that's fair to everyone: base URL in an env var โ€” dictionaryapi.dev in production behind a cache, your /m/YOUR_ID in .env.test and the dev server. The maintainer stops paying for your hot reloads; your CI stops inheriting their bad hours. Current status of the real API: /status/dictionaryapi. Related: Open Library, Quotable, mock any third-party API.

โšก Skip the terminal: paste your captured JSON in the dashboard importer (CSV, OpenAPI, db.json, Postman, and HAR work too) โ€” or create a seeded sandbox in one click. No signup.