Skip to content
Observability for a Solo Developer: Know It Broke Before Users Tell You
Milos KnezevicMilos Knezevic20 Oct 2026 · 11 min read

Observability for a Solo Developer: Know It Broke Before Users Tell You

A solo SaaS monitoring stack on free tiers: uptime checks, Sentry error triage, cookieless analytics — and the 15-minute weekly review that keeps it honest.

Enterprise observability assumes a team you do not have

Almost everything written about observability is written for companies. Distributed tracing across forty services, Datadog dashboards nobody looks at, SLO review meetings, a PagerDuty schedule. The advice is not wrong — it is priced for an organisation where monitoring is somebody's job. For you it is nobody's job. It has to work anyway.

That inverts the design constraints. A platform team optimises for depth of insight and accepts cost and maintenance as the price. A solo developer needs the opposite: maximum signal, near-zero cost, and almost no upkeep, because the person on call is the same person writing the features, answering support email, and doing the accounting. Any tool that needs weekly tending will be abandoned by the third week.

A solo SaaS monitoring stack therefore has to earn its place by answering exactly three questions: is it up, what broke, and what are users doing. Everything beyond those three is optional. I treat monitoring as the final item on the checklist to ship a Next.js app to production — and the only item that keeps working for you after launch day.

Is it up? Two checks, not twenty

Uptime monitoring is the cheapest insurance in software. Services in the UptimeRobot class will request a URL every five minutes and email you when it stops responding — free, indefinitely, with nothing to maintain. The only real decision is which URLs to check.

Check two. The first is your homepage, because that is what a user sees. The second is a health endpoint, because the homepage can lie. On Vercel and most modern hosts your homepage is static or cached at the edge, and the CDN will keep serving it happily while your application code fails on every request that matters. A green check on a cached page proves the CDN is up — which was never in doubt.

A health endpoint forces real code to execute on every probe:

app/api/health/route.ts
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

export async function GET() {
  return NextResponse.json(
    {
      status: "ok",
      commit: process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? "local",
      time: new Date().toISOString(),
    },
    { headers: { "Cache-Control": "no-store" } },
  );
}

The commit hash matters more than it looks. When something is wrong, the first question is always what is actually deployed right now — and a health endpoint that answers it saves you from digging through the hosting dashboard while stressed.

Notice what this endpoint does not do: it does not ping the database, the email provider, or the CMS. That restraint is deliberate. A health check that probes every dependency reports the worst state of any of them, and a degraded database is not the same thing as the site being down. On this site, public pages never touch Postgres at all — content comes from the CMS, and the database only serves newsletter sign-ups and the contact form. If the health endpoint probed Postgres, a paused free-tier database would page me about a total outage that not a single visitor experienced.

Dependencies with their own failure modes deserve their own checks. The database here gets a separate /api/keepalive route, hit by a daily Vercel cron — partly to watch it, mostly because free-tier Postgres pauses after a week of inactivity and one trivial SELECT 1 a day keeps it awake. Two endpoints, two failure domains, two different reactions when one fires.

warning

Resist the deep health check. The moment /api/health aggregates five dependencies, every wobble in any of them reads as a full outage, you stop trusting the alert, and you have manufactured your own alert fatigue. One endpoint per failure domain.

What broke? Error tracking and the triage rule

Uptime checks catch the site being down. Almost nothing that goes wrong in production takes the site down. The far more common failure is one route throwing on a payload you never tested, for a few percent of users, silently. Without error tracking those failures are invisible until a user is generous enough to report one — and most are not.

Sentry's free tier covers a solo product comfortably: a few thousand events a month, which is either plenty or a sign you have bigger problems than the quota. The Next.js SDK wires itself in with one command and, critically, uploads source maps during the build — so a production stack trace points at your actual TypeScript instead of a minified one-liner inside chunk-7f3a.js. An error report you cannot read is decoration.

Gate the whole thing on an environment variable. This site initialises Sentry only when a DSN is present, which means local development and CI never send an event and a noisy dev loop cannot burn the quota:

sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";

const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;

// No DSN, no initialisation — local dev and CI stay silent.
if (dsn) {
  Sentry.init({
    dsn,
    environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "unknown",
    tracesSampleRate: 0.1,
  });
}

The tooling is the easy half. What decides whether error tracking earns its keep is a discipline: every new production error gets triaged, and only three verdicts exist. Fix it, because it is a real bug. Mute it, because it is noise — a bot probing /wp-admin, a browser extension injecting garbage — and you mute deliberately with an ignore rule, not by averting your eyes. Or accept it, because it is real but rare and the fix costs more than the harm, and you write that decision down. The verdict that does not exist is ignoring it. An inbox holding forty unexamined issues is an inbox you have stopped reading, and the one error that matters will sit in it unnoticed.

Alert fatigue is the real enemy here, more than any individual bug. If everything pages you, nothing does — within two weeks you will swipe the notifications away unread, and the system will have trained you to ignore the exact signal you built it for. Configure alerts for two events only: a brand-new issue, and a regression of something you marked fixed. Volume spikes on known issues can wait for the weekly review.

And when the alert fires minutes after a deploy — by far the most common timing — resist debugging in place. The fastest fix is usually going back to the version that worked; you can diagnose calmly afterwards.

The third question is not about failure at all. Referrers, top content, and conversions are how you decide what to build and write next; without them you are optimising blind.

The default answer, Google Analytics, is the wrong tool for a solo operator in the EU. GA4 is built for ad attribution across properties: consent mode, data-processing agreements, an interface designed for marketing teams running campaigns. You will use a tenth of it. And because it sets identifiers and profiles users, serving EU visitors means a consent banner — which costs you conversions, costs you performance, and makes the first thing a new visitor sees a legal dialog.

The Plausible and Umami class of tools answers the actual question instead. They are cookieless — no identifier stored on the visitor's device, no cross-site profiling — which under current EU practice means no cookie banner; a sentence in your privacy policy replaces a legal workflow. The script is around one kilobyte against GA's tens, so it never shows up in a performance audit. And the entire interface is one page: visitors, referrers, top pages, goals.

Goals are the part worth setting up on day one. Pageviews flatter you; conversions inform you. For this site the conversion is a newsletter signup, and knowing which referrer produces subscribers is worth more than every other chart combined. Umami is open source and free if you self-host; Plausible is paid but costs less per month than a lunch and is maintained for you. Self-host if you enjoy running things, pay if you would rather not — both are the right call.

info

Cookieless does not mean exempt from GDPR — it means no consent banner is required, because nothing is stored on the device and no profile is built. You still name the tool in your privacy policy. That one sentence is the entire compliance burden, which is rather the point.

The thread that ties an alert to a log line

Three tools that do not talk to each other produce a familiar, miserable hour: Sentry emails you that something broke, your logs know what happened, and you spend forty minutes scrolling timestamps trying to connect the two. The fix is a convention, not another tool: a request ID.

Generate an ID at the top of every request that does real work. Tag the Sentry event with it, include it in every log line that request emits, and return it in the error response — so a user who emails you a screenshot is unknowingly handing you the exact thread to pull:

app/api/contact/route.ts
import { NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
import { logger } from "@/lib/logger";

export async function POST(request: Request) {
  const requestId = crypto.randomUUID();
  try {
    // validate the payload, send the email...
    return NextResponse.json({ ok: true });
  } catch (error) {
    Sentry.captureException(error, { tags: { requestId } });
    logger.error("contact.send_failed", { requestId });
    return NextResponse.json(
      { error: "Something went wrong.", requestId },
      { status: 500 },
    );
  }
}

Now the path from alert to cause is mechanical: open the Sentry issue, copy the request ID, search the logs, read the story in order. This only works if your logs are searchable key-value events rather than free-form prose — that discipline is its own topic, covered in structured logging for Next.js. The two practices are halves of one system: the logs are the narrative, the request ID is the page number.

What $0 buys you, and the first thing worth paying for

Stacked up, the free tiers cover more than you would guess:

  • Uptime: two HTTP checks every five minutes with email alerts — free at every provider in the category
  • Errors: Sentry's free tier, a few thousand events a month with source maps and release tracking
  • Analytics: Umami self-hosted for nothing, or a Plausible-class host for a few euros a month
  • Logs: whatever your host keeps — on a free hosting plan, retention measured in hours, not weeks

That last line is the catch. The $0 stack tells you that something broke and roughly what — but if you do not look quickly, the logs around the error are gone. An error event without its surrounding logs is a mystery novel with the middle torn out: you know the ending and have to guess the plot.

So the first money I would spend on observability is log retention. A log drain to a service that keeps a week or two of history costs a few euros a month and converts hoping you can reproduce the bug into reading exactly what happened on Tuesday. Not APM, not dashboards, not a status page — logs first. Almost everything else on the paid menu is a team-sized solution to a team-sized problem you do not have yet.

The fifteen-minute weekly review

None of this works without one habit, and it had better be a small one, because elaborate processes are the first thing a solo developer drops under pressure. Fifteen minutes, once a week, same morning every week. Mine looks like this:

  • Uptime report: any incidents? If a blip was real, find the cause; if it was a false alarm, fix the check so you keep trusting it
  • Sentry inbox to zero: every new issue gets its verdict — fix, mute, or accept. Five minutes, most weeks
  • Analytics: top referrers and the conversion number, with one question — did anything surprising send people here?
  • One sentence in a notes file: what changed, what to watch next week

The review is also what keeps the alerts trustworthy. Muting noise weekly means the pager only fires for real problems, which means you keep answering it. The entire system rests on that loop staying intact.

If you are starting from nothing, the order is fixed. Add /api/health and point a free uptime check at it and at your homepage today — twenty minutes, including signup. Install Sentry behind an env-gated DSN this week. Add cookieless analytics with one conversion goal on your next deploy. Then put the fifteen-minute review in your calendar before the tools get a chance to rot. The whole stack costs nothing, takes an evening, and changes the question from wondering whether the site is okay to knowing before any user does — which, when the on-call rotation is just you, is the difference between running a product and hoping about one.