From Side Project to Production: The Monitorion Journey
Monitorion
Monitoring Insights
Every product starts with a spark of frustration. For Monitorion, that spark was an expired SSL certificate that took down a payment flow for four hours because the monitoring tool we were paying for failed to alert us. What followed was a twelve-month journey from a scrappy side project to a production-grade platform handling thousands of checks per minute across multiple continents. This is the story of how we built it, the technical decisions we wrestled with, and the hard lessons we learned along the way.
Choosing the Tech Stack
The first decision was the framework. We chose Next.js with the App Router and React Server Components. The reasoning was pragmatic: server-side rendering for the marketing pages and SEO, server actions for mutations, and a unified codebase for both the dashboard and the API. No separate backend service to deploy and maintain. One repository, one deployment pipeline, one set of dependencies to audit.
For the database and authentication layer, Supabase was a natural fit. PostgreSQL gives us relational integrity for the complex relationships between users, projects, monitors, checks, incidents, and alert channels. Row Level Security (RLS) policies enforce data isolation at the database level, which means a bug in application code cannot leak one customer's data to another. Supabase Auth handles email/password, magic links, and OAuth with minimal custom code.
The Job Queue Decision
The job queue was the most critical architectural decision. Monitoring is fundamentally a scheduling problem: thousands of monitors, each with their own interval, need to fire checks reliably and on time. We evaluated several options — pg-boss for a PostgreSQL-native solution, Temporal for workflow orchestration, and BullMQ for a battle-tested Redis-backed queue. We chose BullMQ because it gives us delayed jobs, retries with exponential backoff, rate limiting, priority queues, and — crucially — named workers that can process jobs concurrently across multiple machines.
The scheduler runs every 10 seconds, queries the database for monitors that are due, and enqueues jobs into the appropriate queue. We implemented a three-tier routing system: multi-region checks for network-sensitive types like HTTP, Ping, WebSocket, GraphQL, and JSON API; single-region checks for types with globally consistent results like DNS, Port, SSL certificates, and FTP; and a dedicated Chrome queue for screenshot, Lighthouse, and multi-step monitors that require a browser. This routing ensures that heavy Puppeteer jobs do not block lightweight HTTP checks, and that checks that benefit from geographic diversity actually run from multiple locations. Single-region checks automatically retry on a different worker if one fails.
Architecture Overview
The system has four main components that work together:
- Next.js application — serves the dashboard, marketing pages, API routes, and server actions. Deployed as a standalone Node.js server behind Nginx in PM2 cluster mode with two instances for zero-downtime restarts.
- Scheduler worker — runs every 10 seconds, queries for all monitors that are due for a check, and enqueues jobs into BullMQ with the appropriate priority and region routing. It explicitly filters out monitors assigned to private workers to prevent cloud workers from duplicating checks.
- Check workers — distributed across five regions (US-East, US-West, EU-Central, EU-East, and AP-Southeast). VPS workers in Vilnius run with Chrome installed for browser-based checks. Fly.io workers in Frankfurt, Virginia, Los Angeles, and Singapore handle standard protocol checks.
- Aggregation and retention workers — roll up raw check data into hourly and daily summaries for the response time charts, and prune old data based on each customer's subscription tier (7 days for free, up to 365 for Agency).
The Multi-Region Challenge
Single-region monitoring is easy. Multi-region monitoring is where things get interesting. When a check from Virginia reports "down" but Singapore reports "up," what do you tell the user? A naive approach — alert on any failure — generates constant false positives. Our answer is consensus-based alerting. A monitor transitions to "down" only when a configurable threshold of regions agree. This virtually eliminates false positives caused by transient network issues at a single checkpoint.
Connecting Workers Across Continents
Running workers on Fly.io required solving a connectivity problem: our Redis instance runs on the primary VPS in Frankfurt, and Fly.io containers need to reach it securely without exposing Redis to the public internet. We use a Cloudflare tunnel that maps redis-tunnel.monitorion.com to localhost:6380 inside each Fly.io container. The worker's entrypoint script starts the tunnel daemon, waits three seconds for it to establish, and then launches the Node.js check worker. It is simple, secure, and has been rock-solid in production — zero connectivity issues since launch.
The deploy process for Fly workers is also streamlined. We compile TypeScript workers into plain JavaScript with npm run build:workers, copy the output to the Fly deploy directory, and run fly deploy. A single command redeploys all three Fly workers in under five minutes. For the VPS workers, our deploy script builds in a temporary directory and swaps the output atomically, so users never see a "Something went wrong" error during deploys.
Fighting False Positives
False positives are the single biggest threat to a monitoring platform's credibility. If your tool cries wolf twice, your team starts ignoring alerts — and then misses the real outage. We invested heavily in reducing them through multiple layers of defense:
- Multi-region consensus — a monitor only transitions to "down" when multiple geographic checkpoints agree, filtering out localized network issues.
- Automatic retries — before marking a check as failed, the worker retries once with a short delay. Transient DNS hiccups, TCP connection resets, and TLS handshake timeouts often resolve on retry.
- Incident deduplication — a unique partial index in PostgreSQL on
(monitor_id, status) WHERE status = 'open'prevents multiple workers from creating duplicate incidents when checks fire simultaneously from different regions. - Configurable thresholds — users can set how many consecutive failures trigger an alert, so a single blip does not page the on-call engineer at 3 AM.
- Smart timeout handling — different check types have different timeout behaviors. A DNS lookup should resolve in under 5 seconds; a Lighthouse audit might legitimately take 30. We tuned default timeouts per check type to avoid false timeouts on slow-but-healthy services.
Lessons Learned the Hard Way
Building a monitoring platform from scratch taught us things that no documentation or blog post could have prepared us for. Here are the lessons that cost us the most time:
Supabase joins have sharp edges. Multi-level joins on self-referencing foreign keys silently return null instead of throwing an error. We spent a full day debugging a feature request page where merged tickets showed as empty, only to discover that the nested .select() syntax simply does not work for self-referencing FKs. The fix was to split complex queries into separate calls and assemble the data in application code. We also learned to never use ternary operators inside .select() — Supabase's type parser breaks silently.
Screenshot checks are resource-hungry. Puppeteer with bundled Chromium works, but it needs careful memory management. Each screenshot check launches a headless Chrome instance, navigates to the page, waits for load plus a configurable settle delay for JavaScript-heavy SPAs, and captures a full-page image. We cap screenshot size at around 500 KB, store them as base64 in the checks table metadata, and — critically — never fetch the metadata column when loading the check history list. Without that optimization, a page of 20 screenshot checks would try to load 10 MB of base64 data.
Redirect checks need millisecond timeouts. An early bug stored the timeout in seconds instead of milliseconds in the configuration object. This caused redirect-chain checks to set a 30,000-second timeout (8.3 hours) instead of 30 seconds. The check would appear to hang, the worker would eventually kill it, and the result would be marked as a timeout failure. The fix was a one-line change, but the lesson was permanent: always be explicit about units, and add comments when a config value uses non-obvious units.
SSRF prevention matters on day one. The first version of our HTTP check worker would happily fetch http://169.254.169.254/latest/meta-data/ if a user entered it as a monitor URL. We implemented isPrivateTarget() early — it blocks RFC-1918 addresses, loopback, link-local, and cloud metadata endpoints. Private workers intentionally skip this check, because monitoring internal services is exactly what they are for.
Our Development Philosophy
We ship fast and iterate. Every feature starts as the simplest version that solves the problem, and we refine based on real usage data. We do not build features that nobody has asked for, and we do not optimize code that is not a bottleneck. The monitoring dashboard loads fast because we use React Server Components for the initial render and stream interactive components. The check history uses paginated load-more instead of infinite scroll because the former is simpler to implement and easier to reason about.
We also believe in owning the full stack. We run our own VPS servers, manage our own Nginx configuration, and deploy with a bash script instead of a CI/CD platform. This gives us complete control over the deployment process and eliminates an entire class of "works locally but breaks in CI" problems. When something goes wrong at 2 AM, we SSH into the server and fix it — no waiting for a support ticket with a platform provider.
What Is Next
We are expanding to more regions — Tokyo and Sao Paulo are next. We are building a public REST API so customers can manage monitors programmatically. We are working on smarter alerting: anomaly detection based on historical baselines instead of static thresholds. And we are exploring a migration of screenshot storage from the database to Supabase Storage for better performance at scale. The foundation is solid. Now we scale.
Ready to get started? Sign up free — 10 monitors, 6 check types, no credit card required.
Enjoyed this post?
Get monitoring tips and product updates delivered to your inbox.