FeedFilters
Pricing Log in Sign up

Blog

  • All posts
  • Subscribe (Atom)

Recent

  • Where the cookie boundary didn't May 12, 2026
  • A CDN for most of it May 11, 2026
  • Done, not abandoned May 8, 2026
  • There's no catch May 7, 2026
  • Sixteen mockups May 6, 2026
  • From scripts to infrastructure May 5, 2026
  • Doing mail myself May 4, 2026
  • Easier now than later May 2, 2026

Archive

  • 2026 15 posts

Easier now than later

May 2, 2026 · Kyle Cronin

Load testing showed me where the walls were. Most of the work that came out of it was about pushing those walls out, which is what the previous post covers. But a separate batch of work landed in the same window that wasn’t really about capacity. It was about giving the service room to grow without painful migrations later.

The thread tying these decisions together is asymmetry. Each one is nearly free to do up front and expensive (or impossible) to do once the service has subscribers, deploy history, or other apps running alongside it. “Now or never” is the honest reason these landed when they did.

The feeds subdomain

The most load-bearing of these is splitting feed serving onto its own subdomain. In production, FeedFilters now serves the app and admin surfaces at feedfilters.net and feed output at feeds.feedfilters.net. Today, both hostnames route to the same backend container by host-header dispatch — a few lines in the router. Tomorrow, if feed output ever needs more capacity than the app box can deliver, that subdomain can move to its own dedicated host (or hosts) without breaking anyone.

“Without breaking anyone” is the whole point. RSS readers remember subscription URLs. Once a user subscribes, their reader has the URL pinned; if I later need to change the URL shape to move feed serving elsewhere, every subscriber’s reader has to re-subscribe to the new URL. That’s the kind of operational debt I’d like to avoid.

Caddy as a shared reverse proxy

The deployment shape FeedFilters runs in is “one box, many apps.” A Caddy reverse proxy on the host owns ports 80 and 443 and routes to backend containers based on labels they declare in their compose files. Each app says, in effect, “I serve feedfilters.net, www.feedfilters.net, and feeds.feedfilters.net; here’s my upstream port.” Caddy reads the labels off the shared caddy docker network, sorts out TLS automatically, and dispatches.

The reason this matters for scale isn’t the load — FeedFilters fits on a single Linode Nanode comfortably. It’s that my server exists as a place for more apps over time. When the next app arrives, it joins the same caddy network, declares its hostnames, and ships. No new TLS work, no new reverse-proxy config to maintain, no per-app port management. The first app pays for setting up the shared proxy; every app after that gets it for free.

Versioned static assets

In the same spirit, every URL emitted into a template that points into /static/ — the stylesheet, the favicon, the chart.js bundle, the fallback icons — now carries a server-start nanosecond timestamp as a query parameter. A new binary on deploy mints a new timestamp, which mints new URLs, which forces every client to refetch.

Because URLs change on deploy and old ones simply stop being requested, the cache headers on /static/* go from a conservative five-minute TTL to one-year immutable. Browsers cache aggressively, revalidate nothing in steady state, and pick up the new asset automatically the next time a page loads. This is the standard pairing for hashed or versioned static assets, but moving to it required the URL versioning to be in place first.

Operational discipline

Two smaller pieces in the same vein, each cheap to do once and worthwhile permanently.

The compose file’s healthcheck now hits /healthz. Without it, a crashed app inside a multi-process container could read as “Up 11 hours” in docker compose ps because supervisord was happily keeping the postfix sidecar alive. The healthcheck makes the operational view honest.

Each successful deploy gets an annotated tag of the form deploy/<timestamp>-<sha>. Back-to-back promotes during a queued deploy can supersede each other — not every commit on the production branch necessarily ships — so the tag is the chronological ledger of what actually went live. git tag -l 'deploy/*' is a workable audit trail.

The instinct underneath

Looking at the whole batch together, there’s a thread I want to call out. None of these are really about adding capacity. They’re about reducing the number of things I have to keep tuning, keep remembering, or keep working around when the service grows. The previous post described a load shedder that reads memory pressure directly and a parsed-feed cache that sizes itself by body length; this post adds a deployment shape that doesn’t need per-app reverse-proxy tweaks, a static-asset story that doesn’t need cache-bust commits, and a feeds origin that won’t break subscribers when the topology underneath it changes.

The instinct under both posts is the same. Build the parts of the system that are likely to need to grow so that growing them doesn’t require remembering anything. If I ever do need to scale FeedFilters up, I want the answer to be “give it better hardware,” not “spend a weekend unwinding decisions I made on day two.”

About· Help· Blog· Privacy & Terms
FeedFilters by Flat Six Software · © 2026