From Script to Daemon: Architecting a Resilient AI News Radar on an 8GB Mac

Developer from India.
Search for a command to run...

Developer from India.
No comments yet. Be the first to comment.
A deep dive into scaling, reliability, and system design

Introduction Load balancers are fundamental to distributed systems. They determine how evenly traffic is distributed, how failures are handled, and how fast your service can grow. Over a weekend, I built a lightweight but production-style load balanc...

“Every simple system starts as a toy and evolves into infrastructure. The art lies in knowing when to evolve.” 🧭 Why This Article Exists Most system design discussions about URL shorteners stop at “put a DB behind an API.”This piece goes all the w...

TL;DR We had a subtle but critical payroll bug: deductions were getting silently dropped due to timezone mismatches between us (Rippling) and a partner system (Employee Navigator). What looked like a simple effective_date == pst_now() check turned in...

How I scaled a simple crawler into a "Staff-Level" automated research assistant by overcoming flaky inputs, hardware limits, and API rot.
As engineers, we are expected to stay on top of everything: Netflix’s latest architecture, AWS updates, Go 1.23 releases. The fear of missing out (FOMO) is real, but the time to read is nonexistent.
I wanted to solve this. My goal was simple: Build an agent that reads engineering blogs for me and sends summaries to my phone.
But building it on my daily driver (M1 MacBook Air, 8GB RAM) forced me to evolve the design from a
script" to a "daemon." Here is the story of that evolution through four major bottlenecks.
My first instinct was to build a standard web scraper using Colly or GoQuery. I thought, "I'll just fetch the HTML and find the article links."
I immediately hit three walls:
The DOM Stability Problem: Tech blogs (especially Medium-based ones like Netflix's) use dynamic class names like <div class="x7-y8-z">. Every time they deployed a UI update, my crawler broke.
The JavaScript Wall: Many modern blogs (Uber, DoorDash) render content via React/Hydration. A simple http.Get returned an empty skeleton, forcing me to consider heavy tools like Selenium or Playwright.
The Resource Tax: Running a Headless Browser (like Chrome) to scrape 5 sites consumes ~1GB of RAM. On an 8GB machine, that’s 12% of my total memory just to find a URL.
| Strategy | Pros | Cons | Verdict |
| HTML Scraping | Can get everything | Brittle; breaks on UI changes | ❌ Too High Maintenance |
| Headless Browser | Renders JS perfecty | Heavy CPU/RAM usage; slow | ❌ Too Heavy for M1 Air |
| RSS / Atom Feeds | Standardized XML | Limited to feed content | ✅ The Winner |
I pivoted to RSS Feeds.
Why: It is a standardised XML contract. It doesn't care about CSS classes, React, or ads.
Efficiency: Parsing 10 XML feeds takes milliseconds and kilobytes of RAM, compared to seconds and gigabytes for headless browsing.
Code: I swapped 200 lines of fragile HTML parsing for the robust gofeed library.
With the links secured, I tried to summarise them locally using Ollama and Llama 3 (8B).
The Crash:
My 8GB M1 Air immediately choked. The OS takes ~3GB, VS Code takes ~1GB. Loading an 8B parameter model (which needs ~4GB+ VRAM) left zero room for the Go compiler. My laptop turned into a heater, and the "summarisation" took 45 seconds per article.
I realized that Hardware Constraints dictate Architecture. I refactored the system to use the Strategy Pattern, allowing me to swap the "Brain" of the agent.
I moved from local inference to Google Gemini (Flash model).
Cost: $0 (Free tier).
Latency: 2 seconds (vs 45s).
RAM Usage: <50MB.
With RSS (fast) and Gemini (cloud), my agent became too efficient. It grabbed 15 URLs and fired 15 concurrent requests to Gemini.
The Crash:
429 Too Many Requests. The free tier limits you to ~15 Requests Per Minute (RPM), and sometimes 5 RPM for newer models. My agent crashed instantly.
I couldn't just "try again." I needed to design for the constraint.
Exponential Backoff: If the API says "Stop", we wait 2s, then 4s, then 8s.
The Speed Bump: I added a calculated delay in the main loop to mathematically guarantee compliance.
Go
// Staff-Level Resilience: Don't just hammer the API
cfg.RequestsPerMinute = 4 // Ultra-safe limit
safeDelay := time.Minute / time.Duration(cfg.RequestsPerMinute)
for _, job := range jobs {
go worker(job)
time.Sleep(safeDelay) // The "Speed Bump"
}
I hardcoded the model string "gemini-1.5-flash" into my source code. One morning, I woke up to 404 Model Not Found. Google had deprecated the alias, and my binary was useless until I recompiled it.
I learned that Dependencies change faster than Code. I refactored the initialization logic to pull the model version from Environment Variables (GEMINI_MODEL). Now, when a model is deprecated, I just update my .env file—no recompile needed.
Today, the system is a robust background daemon that I trust.
Inputs: RSS Feeds (Polled every 6 hours).
State: A simple history.json file prevents re-reading old articles.
Brain: Gemini Flash (Configurable).
Output: Telegram Notifications.
Inputs Matter: Don't scrape HTML if an XML feed exists. Reliability > "Getting everything."
Respect Constraints: If you have 8GB RAM, you can't run Llama 70B. Move the compute.
Resilience > Speed: A slow crawler that never crashes is infinitely better than a fast one that dies on the 10th request.
You can check out the open-source code here: https://github.com/AkshayContributes/crawler-agent
#Go #SystemDesign #WebScraping #AI #Engineering #SideProject #Gemini