Search Without a Server

Add a search bar. Don't add anything else.

← All posts

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:

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:

PUBLIC INTERNET User / Browser AWS 53 Route 53 (DNS) CF CloudFront (CDN) /static /api/search S3 S3 (static site) API API Gateway (REST + auth + rate limit) λ Lambda (query) parses, formats, returns OpenSearch (3-node cluster, t3) ~$95/mo INDEXING PIPELINE EB EventBridge (S3 PUT events) λ Lambda (indexer) parse, tokenize, push SUPPORTING SERVICES (always) CloudWatch IAM (4 roles) Secrets Manager VPC + subnets ACM (TLS) X-Ray ~$110-200/mo + ops time
A typical AWS architecture for a website with search. Two Lambdas, an OpenSearch cluster, an API Gateway, an EventBridge pipeline, and a half-dozen always-on supporting services to keep it all secure and observed. Not wrong — just expensive for fifty pages.

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.

html json { "href":... "title":... "body":... }
HTML in. JSON out. The bronto already eats every page — now it tastes them too.

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:

PUBLIC INTERNET User / Browser AWS 53 Route 53 (DNS) CF CloudFront (CDN) S3 S3 (one bucket) static HTML · nav.html · style.css search.html · search-index.json λ Lambda (BrontoCMS) rebuilds spine pages, nav.html, search-index.json PUT writes SUPPORTING SERVICES (none required) $0/mo extra. No ops. No cluster. search runs HERE, on the user's CPU
A bucket and a Lambda. The Lambda writes the index. The browser reads it.

When this would NOT work

This pattern has limits. It would not work for:

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.

The browser is the server now. Always was, kind of.

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.