Remember that ZZZ code aggregator I built? Well, I looked at my VPS metrics and realized something dumb: I was running Python on every single request for data that updates once per hour.
So I ripped out Flask entirely. Here's what happened.
The Problem
My original setup was standard Flask + Gunicorn behind Nginx:
Request → Nginx → Gunicorn → Flask → Response
It worked fine. But the site was just... rendering the same JSON data every time. The codes only change when HoYoverse drops new ones (roughly weekly, sometimes monthly). Running a Python process for every visitor felt wasteful on a $3 VPS.
Memory usage hovered around 100MB. Not terrible, but not great either.
The Fix: Static File Generation
The new architecture is embarrassingly simple:
Request → Nginx → static file (HTML/JSON)
A background daemon generates static files every hour. Nginx serves them directly. That's it.
The key insight: if your data doesn't change between requests, don't compute it between requests.
The Code That Made It Work
Atomic File Writes
The trickiest part was ensuring users never see a half-written file. Linux gives us atomic renames, so:
def atomic_write(filepath: Path, content: str) -> None:
temp_path = filepath.with_suffix(".tmp")
try:
temp_path.write_text(content, encoding="utf-8")
temp_path.rename(filepath) # Atomic on Linux
except Exception as e:
if temp_path.exists():
temp_path.unlink()
raise e
Write to a temp file, then rename. The rename is atomic - a reader either gets the old file or the new file, never a partial write.
Scheduler Config
APScheduler handles the hourly updates, but I needed to prevent overlapping runs if one takes too long:
scheduler.add_job(
update_codes_task,
"interval",
minutes=60,
max_instances=1, # Prevent overlapping
coalesce=True, # Combine missed runs
)
max_instances=1 ensures only one update runs at a time. coalesce=True means if the server was down and missed 3 runs, it only runs once when it comes back up (not 3 times in a row).
The Results
| Metric | Before (Flask) | After (Static) |
|---|---|---|
| Memory | ~100MB | ~20MB |
| Requests/sec | ~100-500 | ~10,000+ |
| Dependencies | flask, gunicorn | jinja2 |
That requests/sec number is from Nginx serving static files directly. Your mileage will vary based on file size and server specs, but the point is: it's way faster.
Bonus: Image Optimization
While I was at it, I converted all the reward icons from remote PNG URLs to self-hosted WebP files:
const ICON_MAP = {
'Polychrome': '/static/e6ee639872c119aa6895758f3a755d3b.webp',
'Denny': '/static/7db931d2138edcfb9e155907503f2fbe.webp',
'Senior Investigator Log': '/static/ab0406e53b7f8c4afe08096a2f7aa587.webp',
// ... 11 reward icons total
};
The annoying part? I was loading these from game8.co on every page view. Now they're:
- 25-34% smaller (WebP compression vs PNG)
- Self-hosted (no external dependencies)
- Content-hashed for cache-busting
I kept PNG fallbacks for older browsers, but modern browsers get the lighter WebP versions.
Unexpected Benefits
Crash-proof: If my Python daemon dies, the last-generated files keep getting served. Users see stale data (worst case: 1 hour old) instead of an error page.
Simpler deployment: No WSGI server to configure. Just run the Python script as a systemd service and point Nginx at a directory.
IndexNow integration: When codes actually change, I ping search engines. But it only happens when there's real new content, not on every request.
The Takeaway
The best optimization is often eliminating unnecessary work entirely.
Before: Run Python interpreter → load Flask → route request → fetch cached data → render template → return response.
After: Return file.
This pattern works whenever your data updates less frequently than your traffic. Blog posts, documentation sites, dashboards with hourly data - all good candidates.
Cost Breakdown
Still the same as before:
- Domain: ~$10/year
- VPS: ~$3/month
- Cloudflare: Free tier
zenlesscodes.com - still aggregating ZZZ codes, now 100x faster at it.
Top comments (0)