<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://blog.byteslab.io/</id>
    <title>Bytes Lab</title>
    <updated>2026-06-04T12:25:43.405Z</updated>
    <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
    <author>
        <name>Bytes Lab</name>
        <uri>https://blog.byteslab.io/</uri>
    </author>
    <link rel="alternate" href="https://blog.byteslab.io/"/>
    <link rel="self" href="https://blog.byteslab.io/atom.xml"/>
    <subtitle>An open engineering journal by Bytes Lab — raw deep-dives into backend architecture, system design, and the hard lessons learned while shipping real software.</subtitle>
    <rights>Copyright © 2026 Bytes Lab</rights>
    <entry>
        <title type="html"><![CDATA[The Architecture Behind a Minimalist Uptime Monitor]]></title>
        <id>https://blog.byteslab.io/posts/still200-system-architecture/</id>
        <link href="https://blog.byteslab.io/posts/still200-system-architecture/"/>
        <updated>2026-06-02T21:05:28.532Z</updated>
        <summary type="html"><![CDATA[I recently worked on and launched a product that I'm incredibly proud of. I created Still200, a minimalist API uptime monitoring tool built...]]></summary>
        <content type="html"><![CDATA[<h2>Introduction</h2>
<p>I recently worked on and launched a product that I'm incredibly proud of.
I created <a href="https://still200.com">Still200</a>, a minimalist API uptime monitoring
tool built specifically for indie devs.</p>
<p>The idea was born out of a sudden moment of developer panic.
One afternoon, I needed to generate some art and opened my AI image generation app, <a href="https://illumity.app">Illumity</a>.
Nothing loaded. I force-closed and reopened the app but still, a blank screen.
This had never happened before.</p>
<p>I opened my MacBook, launched a terminal and pinged my root endpoint directly.
It returned a <code>502 Bad Gateway</code> error. That was when it hit me something was wrong with my server.
I logged into my Railway account and restarted the API service. One minute later
everything was back online. To be honest, I'm not sure what had gone wrong or for how long it was down.</p>
<p>I immediately went looking for a simple tool to monitor my APIs.
I looked at existing tools but they were either too bloated with enterprise features
I didn't need or too expensive for an indie hacker's budget. That was my light-bulb moment.</p>
<p>I decided to build my own simple platform that could alert me not only when the API is down,
but also when other critical backend components e.g. Redis, Postgres, were down.</p>
<h2>The Architecture</h2>
<p>For the system architecture, I wanted to keep things simple. Not <em>easy</em>, simple!
That meant minimal moving parts and an architecture I could jump into and debug
completely on my own if things went sideways at 2 AM.
Here are the components of my architecture that powers Still200:</p>
<ul>
<li><strong>The User Interface (iOS)</strong> - Built natively with Swift and SwiftUI,
and available on the <a href="https://apps.apple.com/us/app/still200/id6770858177">App Store</a>.
I chose to start with a native mobile app for the client interface because I'm conversant with Swift,
I find it elegant to write, and offers an incredible user experience.</li>
<li><strong>API</strong> - A purely async REST API built with FastAPI, Pydantic schema validation and Python.</li>
<li><strong>Authentication &amp; User Management</strong> - <a href="https://supabase.com/docs/guides/auth">Supabase Auth</a></li>
<li><strong>Database</strong> - PostgreSQL hosted on <a href="https://supabase.com">Supabase</a>.</li>
<li><strong>Schedule keeper and Job Dispatcher</strong> - Instead of pulling in a 3rd party library for
scheduling and task running, I built my own using <a href="https://redis.io/docs/latest/develop/data-types/sorted-sets">Redis sorted sets</a> to manage high-frequency probing with high precision.</li>
<li><strong>Incident Notifications</strong> - <a href="https://nats.io">NATS messaging system</a></li>
<li><strong>Live UI Updates</strong> - Real-time monitoring data streams from the backend to the iOS app for
UI updates using SSE (Server Sent Events) backed by <a href="https://redis.io/docs/latest/develop/pubsub/">Redis pub/sub</a>.</li>
<li><strong>Observability</strong> - <a href="https://pydantic.dev/logfire">Pydantic Logfire</a></li>
</ul>
<p>Here's a high-level illustration of how the pieces fit together before we look at the components
in more detail right below.</p>
<pre><code>flowchart TD
    A["iOS app"]
    B["FastAPI Backend"]
    C["Redis\nZSET"]
    D["Background checker workers"]
    E["Target Endpoints\nAPIs · DBs · Services"]
    F["NATS JetStream"]
    G["Notification Workers"]
    H["APN"]

    A --&gt;|REST API| B
    B -.-&gt;|SSE · Redis pub/sub| A
    B --&gt;|ZADD| C
    C --&gt;|ZMPOP| D
    D --&gt;|httpx ping| E
    D --&gt;|incident| F
    D --&gt;|ZADD| C
    F --&gt;|incident| G
    G --&gt; |POST to APN Server| H
    H -.-&gt;|push alert to user| A
</code></pre>
<h3>The Scheduler</h3>
<p>I looked at a few Python libraries for background jobs, but none quite fit my requirements.
I needed something lightweight with minimal config. The main challenge was configuring a
dynamic number of crons that run on entirely different user-defined schedules.</p>
<p>Celery and taskIQ are great, and I've used them in other projects before but they
did not seem to fit in nicely here no matter how hard I tried to tweak the configurations.
Standard task runners are excellent when you have fixed, pre-defined worker queues.
Forcing them into this specific paradigm felt like fitting a square peg into a round hole.</p>
<p>In my philosophy of keeping things simple, I turned to Redis and sorted sets in particular.
If you're unfamiliar with them, have a look at the <a href="https://redis.io/docs/latest/develop/data-types/sorted-sets/">docs</a>
to understand how they work.</p>
<p>For my use case, I'd add a monitor ID with its score being the next time (epoch unix timestamp)
when its check is due as the score. This would look something like:</p>
<pre><code># ZADD key score member
ZADD monitor_schedule 1780503850 "9e34e9d5-7ca9-482f-b4df-ec1cc2faac62"
</code></pre>
<p>I then have a lean Python script constantly polling the sorted set for due
items. It specifically looks for any members with a score less than or equal to the current epoch unix timestamp.</p>
<p>When a monitor ID is pulled, the scheduler instantly pushes it to an execution queue
for a background worker (checker) to pick up.</p>
<h3>The Checker</h3>
<p>The checker is a decoupled background worker that is responsible for doing the actual HTTP
pings against the user-defined endpoints and determine system health based on the response it receives.</p>
<p>This process needed to be highly concurrent so that it could handle a large number of simultaneous checks.
I leveraged Python's <code>asyncio</code>, paired with <code>httpx</code> - an excellent fully asynchronous HTTP client
to make the actual HTTP calls concurrently without blocking.</p>
<p>One caveat of using Redis lists with <code>LPUSH/BRPOP</code> here is that they're a fire-and-forget system.
Once the checker worker pops an item from that task queue, it's gone. If the worker crashes before
fully performing the check then the job would be lost. To solve this problem and guarantee at-least-once execution,
I introduced another list, <code>processing_queue</code>, that would hold items that are being currently checked.
Once the check completes successfully, the item would be removed from that queue. The next check time is then computed
based on the user's configured check interval for that monitor, and then scheduled right back into the sorted set with the
new future timestamp.</p>
<p>In future, I will introduce another process that can check the <code>processing_queue</code> for items that might
have been sitting there for too long and maybe recycle them into the main task queue.</p>
<h3>Live UI Updates</h3>
<p>To make the iOS app feel alive, I wanted real-time updates to the UI as monitors were checked.
If a user has the app open, they'll notice some monitor details like last check time or even health
status update. This felt like a massive UX win.</p>
<p>To achieve this, I chose SSE (Server Sent Events). In my case data flow is unidirectional - from server to client.</p>
<p>On the backend, Redis came to the rescue yet again. I leveraged <strong>Redis Pub/Sub</strong>, a lightweight,
real-time messaging pattern where publishers send messages to named channels without
knowing who will receive them, and subscribers listen to channels to receive those messages.</p>
<p>Here's how it operates:</p>
<ol>
<li><strong>Publish</strong>: The moment a background worker finishes an endpoint check, it publishes
the serialized payload with the latest check results to a specific Redis channel.</li>
<li><strong>Subscribe</strong>: On the API layer, a dedicated FastAPI streaming endpoint handles open connections from the iOS client. This      endpoint subscribes to the Redis channel at runtime.</li>
<li><strong>Stream</strong>: As soon as a message drops into the channel, FastAPI yields the data, streaming it instantly across the open HTTP connection straight to the client. The iOS app handles the event and smoothly mutates the UI state.</li>
</ol>
<p>One thing to note is that Redis Pub/Sub offers at-most-once-delivery guarantee.
I did not mind this because even if an update is missed, it's nothing critical and the
app does not break. A user would still be able to see up-to-date data if they refresh the monitor list.</p>
<h3>Incidents and Notifications</h3>
<p>Systems fail. That reality is the entire reason I built Still200.
When a worker detects that an endpoint is degraded or unhealthy, users expect to know
immediately that something is wrong so they can act quickly.</p>
<p>Notifications are a very critical part of an uptime monitor.
Users missing notifications or notifications arriving late would mean the app isn't performing its core function.
I had to build a resilient event-driven system with an at-least-once delivery guarantee.</p>
<p>I chose <a href="https://nats.io">NATS</a> (a high-performance, lightweight messaging system)
as the core messaging engine.</p>
<p>I thought about using Kafka given that's what we use at my 9-5 and I'm familiar with it,
but felt it would be overkill for my system.
NATS also has much lower administrative complexity than Kafka. Remember, my goal was to keep things <em>simple</em>.</p>
<p>To achieve message persistence and durability, I enabled NATS JetStream.
This allows me to ensure that notification events are never lost in transit;
a message is only acknowledged and removed from the stream once the notification
worker has successfully dispatched the alert.</p>
<p>When background checker identifies an issue, it triggers a two-step workflow:</p>
<ol>
<li>writes an incident to the Postgres database with details about the failure.
Serves as a historical source of truth.</li>
<li>publishes a message with the incident details to the notifications subject.</li>
</ol>
<p>On the other end of the stream, I have a notifications worker subscribed.
Once a message is received, the details from the data are properly formatted based on the notification type.
Right now, Still200 supports Apple Push Notifications, but I'll be adding more channels
(email, slack, discord) down the road.</p>
<p>The notifications worker, after formatting the payload that APN expects,
does a <code>HTTP/2 POST</code> request to Apple servers which will then deliver the notification
to the user's app. The message from the stream is then acknowledged.
All this happens in <strong>under 500ms</strong>.</p>
<p>NATS has been quite impressive so far.</p>
<h2>Lessons Learnt: What Can Go Wrong in Production</h2>
<p>Building a system that pings a URL and looks for a 200 OK is simple.
Building a system that gracefully handles the absolute chaos of the public internet
without waking a user up with a false alarm at 3 AM is where the real engineering begins.
Here are a few things I've learnt, and how I solved or I'm planning how to solve them:</p>
<ol>
<li>
<p><strong>Endpoint Timeouts</strong>
An endpoint that hangs for 45 seconds without responding is arguably just as broken as an outright 502 Bad Gateway.
If a user's target service experiences an unhandled deadlock and leaves connections open,
your background worker pool can easily get starved. If your worker processes are sitting around waiting on slow endpoints,
they will miss the schedules for healthy endpoints, causing the entire platform to lag.</p>
<p>The fix here was to enforce strict timeouts. <code>httpx</code> easily allows one to do this.
If a target endpoint can't deliver within the set time window, the connection is instantly severed,
the failure is logged, and the worker event loop is freed up for the next job.</p>
</li>
<li>
<p><strong>The False Alarm Problem</strong>
Networks are noisy. A single failed HTTP ping doesn't necessarily mean a backend is dead; it could be a transient routing glitch, a dropped packet, or a momentary load spike on the host. If your system fires an incident alert on the very first failure, your app becomes spammy.</p>
<p>The fix is to implement an explicit unhealthy threshold.
Instead of escalating on a single failure, a failed check increments a counter
inside a Redis hash for that specific monitor. The system only flags an incident and sends a push notification if a monitor fails three consecutive checks.</p>
</li>
</ol>
<p>These were just the first few hurdles of bringing Still200 to life.
As the user base grows and traffic patterns shift, I fully expect to run into new bottlenecks,
weird networking anomalies, and edge cases I haven't even conceived of yet.</p>
<p>But that’s the beauty of building in public. Every hard lesson is just engineering data...and fodder for the next blog post 😃</p>
<h2>What's Next for Still200?</h2>
<p>With the core components in place and fully operational, Still200 is officially live.
But the work doesn't stop here. There's still a lot more to do, and I'm excited to keep building:</p>
<ul>
<li><strong>Lifting the Monitor Limits:</strong> Right now, I have limited the number of monitors to 3 per user as I gather
initial feedback and monitor performance. I will be removing this restriction very soon and also introduce a subscription model.</li>
<li><strong>More Notification channels</strong></li>
<li><strong>Expanding the Ecosystem:</strong> Bringing the interface to the desktop with dedicated web and macOS apps.</li>
<li><strong>Rewrite some components:</strong> I'm especially keeping a close eye on the checker worker.
As the number of concurrent monitors scales into the tens of thousands, Python's runtime memory
footprint and CPU overhead for managing massive event loops can start to climb,
leading to higher infrastructure costs. I might have to rewrite it in Go or Rust for better performance in future.</li>
<li>...and so much more</li>
</ul>
<p>Building Still200 reminded me of the absolute joy of stripping away bloated enterprise
abstractions and solving distributed systems problems with simple, fundamental building blocks.</p>
<p>I hope you enjoyed reading. Happy building!</p>
]]></content>
        <author>
            <name>Bytes Lab</name>
            <uri>https://blog.byteslab.io/</uri>
        </author>
        <published>2026-06-02T21:05:28.532Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Hello World: Building with Intention]]></title>
        <id>https://blog.byteslab.io/posts/hello-world/</id>
        <link href="https://blog.byteslab.io/posts/hello-world/"/>
        <updated>2026-05-25T19:08:48.024Z</updated>
        <summary type="html"><![CDATA[I started Bytes Lab because I missed a certain kind of software. Too much of what we use today feels bloated, sluggish, and overly complicat...]]></summary>
        <content type="html"><![CDATA[<p>I started <a href="https://byteslab.io">Bytes Lab</a> because I missed a certain kind of software. Too much of what we use today feels bloated, sluggish, and overly complicated. It feels like development teams are constantly rushing features to the surface while cutting corners on the foundation.</p>
<p>I wanted to take a different approach.</p>
<p>Bytes Lab is my software studio, a space focused on AI, design, and infrastructure innovation. But more than that, it’s an experiment in a specific architectural philosophy: extreme backend depth paired with minimalist user interfaces.</p>
<p>The way I see it, great engineering boils down to a few core rules:</p>
<ul>
<li><strong>Performance is a feature.</strong> If software isn't fast, low-latency, and rock-solid, it isn't finished.</li>
<li><strong>Complexity should be invisible.</strong> A user interface should be intuitive and dead-simple. All the heavy-duty orchestration belongs under the hood where the user never has to see it, but always benefits from it.</li>
<li><strong>Build out loud.</strong> I appreciate high-concurrency systems, robust data pipelines, and transparency in how things are engineered.</li>
</ul>
<p>That last point is exactly what this blog is for. This isn't going to be a stream of generic marketing fluff. I want this to be an open engineering journal—a place where I can think out loud, share raw architectural deep-dives, and document the hard lessons learned while optimizing infrastructure.</p>
<p>I'm focused on building things the right way, from the ground up. I'm glad you're here for the ride.</p>
<p><strong>Up next:</strong> I’m pulling back the curtain on my flagship project, <a href="https://still200.com">Still200</a>, to show how a complex web of background schedulers, message brokers, and real-time streaming engines power a minimalist uptime monitor. Stay tuned.</p>
]]></content>
        <author>
            <name>Bytes Lab</name>
            <uri>https://blog.byteslab.io/</uri>
        </author>
        <published>2026-05-25T19:08:48.024Z</published>
    </entry>
</feed>