Search Without a Server
Add a search bar. Don't add anything else.
What I added
The site has search now. Type in the bar at the top right, press Enter, get results across every post, case study, and war story on YOU++. Title-boosted ranking. Match snippets with the search terms highlighted. Fuzzy matching, so typos still find what you meant.
It's fast. Genuinely fast — under 50 milliseconds from keypress to first result on screen, every time.
What I didn't add
A server. A database. An Elasticsearch cluster. An API endpoint. A Docker container. A Kubernetes pod. A backend of any kind. A vendor account. A monthly bill.
The site costs the same to run today as it did yesterday: roughly fifty dollars a year for the three domain renewals, plus a few cents a month for CloudFront traffic. Search added zero recurring cost.
What the conventional answer would have been
If you asked someone how to add search to a website, the default architecture goes something like this:
- Set up Elasticsearch (or OpenSearch, Algolia, Typesense, or whatever the current flavor is)
- Build an indexing pipeline that watches your content and feeds new documents into it
- Build an API endpoint that accepts a query and returns ranked results
- Deploy a server (or serverless function) to run the API
- Wire up auth, rate limiting, monitoring, dependency updates, and security patching
- Maintain it forever
For a website at any meaningful scale, that's correct. For a website with fewer than a thousand pages, that's a lot of recurring complexity to do something the user's web browser can already do for itself.
The architecture diagram for the conventional version typically looks something like this:
The actual architecture
Two pieces, neither one a server.
1. BrontoCMS builds the search index.
The same BrontoCMS Lambda that already auto-generates the section pages and the top navigation was taught one new trick: walk every page in the bucket, extract the title, summary, and body text, and write the result to a single JSON file at /search-index.json.
This runs on the trigger that already exists. Any time a page is uploaded, deleted, or modified in S3, the Lambda fires, and the search index gets refreshed automatically. Same convention. Same code path. Same Lambda function. The search index is just one more thing the bronto chews on while it's already eating.
The output is plain JSON — one entry per page with href, title, summary, body, and section. Nothing complicated. Roughly 480 KB raw, about 80 KB on the wire after gzip.
2. The browser does the searching.
The /search.html page loads that JSON file and hands it to a small JavaScript library called MiniSearch (about 12 kilobytes, gzipped). MiniSearch builds a full-text index in the browser tab, on the user's own CPU, in a few milliseconds. As the user types, the library runs queries against that index and returns ranked results with match positions for snippet highlighting.
The user's computer does the search. The user already has a computer. We're just letting it work for itself.
Why this is fine at this scale
There are about fifty pages on this site. Full-text JSON for all fifty fits in 80 KB compressed — smaller than most of the SVG illustrations on the page you're reading. A visitor downloads it once when they hit the search page; the browser caches it after that. CloudFront caches it at the edge, so it serves at edge-network latency.
Querying fifty documents in the browser is faster than a round-trip to a remote search server would be. There is nothing to wait for. No "loading…" spinner. No timeout possibility. The search bar feels like local search because it is local search.
The architecture diagram for this site, by contrast, looks like this:
When this would NOT work
This pattern has limits. It would not work for:
- A site with 100,000+ pages — the index JSON would be too large to ship to every visitor
- A site with private content that shouldn't be sent to the client at all
- A site that needs faceted search across many structured fields
- A site that needs server-side query analytics — "what are people searching for?"
- A site with frequent updates where staleness matters in seconds, not minutes
For those, you genuinely need a server. But for a static site with a few hundred pages of public content and a handful of edits per week, you don't.
The bigger pattern
Most "obviously needs X" architectural assumptions are correct at large scale and badly miscalibrated at small scale. The default answer is calibrated for the largest possible site, because that's what the answer-givers spend most of their time thinking about. If you accept the default, you build a small system with big-system overhead and you maintain it forever.
The trick is to ask: at my scale, does the user's browser have enough CPU to do this? More often than people expect, the answer is yes. The browser is a fast, capable computer that already has the user's full attention. Putting work there is free.
The same pattern explains why the rest of this site exists at all. No CMS. No backend. No application server. No framework. Just S3, CloudFront, a tiny Lambda, and the user's browser doing whatever it's good at — rendering HTML, running small JavaScript libraries, holding a search index in memory while the user types.
What it took
About an hour. One new Python function added to the BrontoCMS Lambda (around forty lines), one new HTML file with embedded CSS and JavaScript (around 200 lines including the search UI), one new line in nav.html to put a search input in the top bar.
That's the whole feature.
The site is faster than most sites with full backend search teams. It cost nothing to build, costs nothing to run, and will continue to cost nothing as long as fifty pages stays roughly fifty pages.
If the page count ever climbs to a thousand, the JSON gets bigger and the load time gets longer and at some point this pattern stops working. That's fine. By then I'll have learned something about what I actually need from search at that scale, and I'll build the next version with the right shape. The pain teaches the spec.
Until then: the bronto already eats every page. Now it tastes them too.
Disclosure: This post is co-authored by Bill and Claude. Bill described the architecture choices and the “don't accept the default” thesis. Claude wrote the prose and built the search feature being described. The whole loop — from "let's add search" to a working search bar on every page — took roughly an hour.