WordSimi needs to search 400,000 words without treating a Cloudflare Worker like a conventional server with a large, permanently resident process. The runtime is intentionally constrained: isolates are short-lived, memory is finite, and CPU and request budgets matter.
The resulting architecture keeps HNSW, moves its native assumptions into WebAssembly, and divides the index into independent semantic shards. A public router Worker coordinates the search. Private Worker services own the shards. Durable Objects give each shard a stable state boundary. There is no Vectorize fallback path: a search either succeeds through HNSW-WASM or returns an explicit error.
This post describes the deployed design, including the trade-offs that make it approximate twice: once inside HNSW and once when routing to a subset of shards.
Why one giant in-memory index is the wrong shape
An HNSW index contains more than the raw vectors. WordSimi’s source vectors alone represent 400,000 words × 50 dimensions × 4 bytes, roughly 80 MB before graph links, labels, lookup structures, allocator overhead, and the WebAssembly runtime are counted.
That estimate explains the pressure. Loading everything into one isolate would leave little practical headroom and make initialization costly. More importantly, a Worker isolate is not a machine you can assume will stay warm forever. Any design that relies on one large global singleton being permanently available will eventually pay the load cost again.
Sharding changes the unit of failure and memory ownership. Each private service loads only its index slice. The total graph can be large while each isolate remains comfortably smaller.
The current topology
The deployed topology has:
- One public router Worker.
- Sixteen private, route-less shard Worker services.
- One SQLite-backed Durable Object class in each service boundary.
- 1,024 deterministic router-coordinator Durable Object identities to distribute coordination load.
- A four-of-sixteen semantic fan-out for an uncached query.
The shards are semantic partitions rather than simple alphabet ranges. Each partition has a centroid in the same 50-dimensional space. The router can compare a query vector with those centroids and choose the most promising partitions.
Sixteen services may sound like a lot, but service count is not the same as sixteen always-running servers. Cloudflare creates isolates as requests arrive. The important consumption is the work each query causes, which is why fan-out is four rather than sixteen.
Resolve the seed before routing
A text query begins with a word, not a vector. The router uses small static hash-bucket assets to locate the word’s owner shard and local ID. It asks the owner shard for the seed vector, which also guarantees that the shard containing the exact input participates in the search.
The router then compares that vector against the sixteen stored centroids. It selects four shards, always including the owner, and invokes them through service bindings. Those private services have no public routes.
Each selected shard searches its local HNSW graph. The router merges candidates, orders them by distance, removes the query word where appropriate, and returns the requested count.
The flow is:
- Normalize and validate the word and result count.
- Resolve the word to its owner shard and local ID.
- Retrieve the seed vector from the owner.
- Select the nearest semantic shard centroids.
- Search four local HNSW graphs in parallel.
- Merge and rank the candidate neighbors.
- Cache the public response.
On an uncached search, that means one coordinator Durable Object request plus four shard Durable Object requests. A cache hit skips the coordinator and the shards entirely.
Where WebAssembly fits
HNSW is the useful algorithm; WebAssembly is the portability layer. The shard build pipeline serializes each HNSW graph in the format expected by the WASM version of hnswlib. At runtime, a shard Worker instantiates the module, places the index in its in-memory filesystem, loads it, and retains the resulting search object for as long as the isolate stays alive.
This preserves the graph search behavior without requiring a native Node add-on in the Workers runtime. The code does not rebuild the graph during a request. Index construction and serialization are offline jobs; production performs loading and querying.
Keeping those phases separate matters for CPU. Building HNSW is expensive and belongs in a controlled data pipeline. Searching a prepared graph is the request-time operation.
Approximation at two layers
HNSW itself is an approximate-nearest-neighbor algorithm. It traverses a layered graph rather than calculating the distance to every point. Parameters such as M, construction effort, and search effort shape the memory, build-time, recall, and latency trade-offs.
WordSimi adds another approximation: shard routing. Searching four semantic partitions is cheaper than searching all sixteen, but a true global neighbor can live in a shard the router did not select.
That choice was measured, not assumed. The selected four-shard routing setup achieved about 91.8% mean recall@20 across a 1,000-query evaluation sample. For an exploratory word-neighborhood interface, that recall is a reasonable exchange for materially lower work per uncached query.
The interface therefore describes results as approximate. It does not suggest that the list is the one exact ordering of language.
Why there is no fallback
A fallback to a different vector engine makes operations look resilient while changing the meaning, cost model, and result distribution of the endpoint. It can also hide a broken HNSW deployment until users notice inconsistent neighborhoods.
WordSimi deliberately has one search mode. Health output identifies it as hnsw-wasm. If a required shard is unavailable or an index cannot load, the request fails visibly and produces structured diagnostics. That makes the contract honest and keeps capacity planning tied to the architecture actually being tested.
Retries are not the same as a fallback. A bounded retry of the same idempotent HNSW operation may be appropriate for a transient platform failure. Switching engines or returning a different dataset is not.
Resource control comes from avoiding work
The largest optimization is not a clever WebAssembly micro-tweak. It is refusing unnecessary search work.
- The static Pages frontend does not invoke a Worker to serve HTML, CSS, JavaScript, or brand assets.
- Input validation rejects unusable queries before shard fan-out.
- Static lookup buckets avoid loading a monolithic word-to-vector map in the router.
- Semantic routing calls four shards instead of all sixteen.
- Shard calls run concurrently.
- Cache hits avoid Durable Objects and graph traversal.
- Private service bindings prevent the shard surface from becoming a separate public API.
Free-tier capacity is still finite. An uncached search consumes more than one platform operation, so the safe way to discuss capacity is in measured requests and cache-hit rates, not as “free forever.” The architecture is designed to make each useful search predictable and to let repeated queries become cheap.
What this design is good for
This pattern fits a read-only or read-heavy index that can be built offline, divided into bounded shards, and queried approximately. It is less attractive when vectors change constantly, strict global top-k accuracy is required, or the workload needs large per-request mutations.
For WordSimi, the index is a versioned artifact and approximation is acceptable. That makes sharded HNSW-WASM a practical match.
Try a search on WordSimi, then read what semantic word similarity actually measures to interpret the neighborhood rather than treating distance as a dictionary definition.
Continue in WordSimi
Test the idea with a word from your own work.
Search one seed word, inspect the nearest semantic neighbors, and validate the promising directions in a dictionary or your draft.
