Goread2 - Chapter 6: The Long Tail
This is the sixth (and mercifully, the last) in a series of posts wherein I attempt to recount the history of Goread2 as it approaches a state in which I might actually try to share it more broadly.
I don’t think mature software announces itself with a precise moment of completion. It just quietly changes character: commits get smaller, features get more specific, problems get subtler. What was once “make it work at all” becomes “make it work better, cost less, and fail more gracefully.”
Migration and Its Consequences
January opened with a clean-looking commit: switch from App Engine Flexible to Standard. Flexible runs on always-on VMs. Standard can scale to zero when idle, so you pay nothing when the app is unused. The migration was a handful of lines in app.yaml.
Costs tripled within days!
Three causes had converged at once. The CPU utilization target had been silently dropped during the migration, making the autoscaler aggressive. Deleting expired sessions asynchronously, which we added that same week to avoid blocking requests, turned out to keep Standard instances alive; a running goroutine is not an idle instance. And three background cleanup tickers had always been running, harmless on Flexible (which never scaled to zero anyway) and fatal to the billing on Standard. The fix was moving cleanup to cron jobs and reverting the async session deletion, which then introduced a data race in the test suite since the mock had not been written for concurrent access. Sometimes, it just be that way.
Steady Work
Once the cost explosion was resolved, work settled into productive incrementalism. Security headers arrived in February: CSP, HSTS, X-Frame-Options, the full set, deployed in report-only mode first so that article images from arbitrary RSS sources would not immediately break. HTTP conditional requests followed: storing ETags and Last-Modified headers from feed servers, then sending If-None-Match on subsequent fetches. A 304 Not Modified response means no re-download, no re-parsing, no Datastore writes. Estimated bandwidth savings: roughly 90% for unchanged feeds.
The most satisfying commits found structural inefficiencies hiding in plain sight. Three separate Datastore operations per feed per cron run (UpdateFeedTracking, UpdateFeedLastFetch, UpdateFeedCacheHeaders), each doing its own Get+Put round trip, were collapsed into a single UpdateFeedAfterRefresh. Per-article URL deduplication queries became one batch query per feed. We added a 90-day time window to that query since feeds do not recycle old URLs; scanning the full article history had always been waste. Each change touched both backends and required test updates, but each measurably reduced the monthly bill.
The goroutine trap appeared one more time in March. The cache stats logger added in February was keeping instances alive. Out it went. By that point the pattern took about thirty seconds to recognize: check the uptime logs, find a suspiciously low scale-to-zero rate, follow the thread back to whatever was ticking in the background.
The commits keep coming. The open issue count stays nonzero. The billing dashboard shows a number in the single digits most months. The app does what it was built to do.
What Would We Do Differently?
GoRead2’s arc traces a pattern that has less to do with AI and more to do with questions. When you are moving fast, the natural prompt is “make this work.” Those prompts get answered. What does not get answered automatically are follow-on questions: is this safe? What does this cost at scale? What keeps this instance alive at 3am?
The TLS bypass existed because the prompt was “make HTTPS work on App Engine,” not “make HTTPS work securely.” The SSRF vulnerability existed because the prompt was “fetch this feed URL,” not “safely fetch a URL a potentially malicious user submitted.” The memory leaks existed because the prompt was “cache these reads,” not “cache these reads in a way that cleans up after itself.” The goroutine that prevented scale-to-zero existed because the prompt was “make this cleanup non-blocking,” not “make this cleanup non-blocking without keeping the instance alive.”
None of this is a failure of the AI. It is a failure of the question.
What worked better: the November code review, where the explicit goal was to find what could go wrong. That review produced nineteen actionable findings in a single session because the question was adversarial by design. Detailed issue specs with file locations, current behavior, and pseudocode for the fix produced better implementations than vague feature prompts. Measuring rather than assuming: “90%+ coverage” in the August commit versus the 9.7% that showed up when someone actually ran the tool in November.
The quality of AI-assisted code is downstream of the quality of the questions being asked. You can build in eight months what used to take years. But speed compounds mistakes as efficiently as it compounds progress. Build “is this secure?”, “what does this cost?”, and “what breaks this?” into the prompting habit from the start rather than discovering the answers expensively later.
tl;dr - ask better questions.