Monolith or microservices: the decision most teams get backwards
Microservices solve an organisational problem with a distributed systems bill. If you do not have the organisational problem, you just have the bill.
A production web request travels through DNS resolution, a TLS handshake, a CDN or load balancer, a reverse proxy, your application server, and then out to databases and caches. Each hop adds latency and a failure mode, and knowing the sequence is what turns 'the site is slow' into a specific, testable hypothesis.
Every one of those steps is measurable. Server-Timing headers let you attribute time to each phase and see it in the browser's network panel — the fastest way to stop guessing.
| Component | Job | Common failure |
|---|---|---|
| CDN | Cache and serve near the user | Cache misses from bad cache headers |
| Load balancer | Distribute traffic, health check | Removing all instances during a bad deploy |
| Reverse proxy | TLS, compression, static files, buffering | Timeouts shorter than the app's slowest route |
| App server | Run your code | Blocked event loop, exhausted workers |
| Database | Persist and query | Connection limit exhaustion, missing indexes |
| Cache | Fast reads for hot data | Stampede after eviction |
At the database, almost always, because each application instance holds a pool and instances multiply.
Ten application containers with a pool of 20 connections each means 200 connections to a database configured for 100. Under autoscaling this fails at exactly the moment traffic is highest. A connection pooler in front of the database — PgBouncer or a managed equivalent — decouples application scaling from database connection limits.
// Liveness: is the process alive? Keep it trivial.
app.get("/healthz", (_, res) => res.status(200).send("ok"));
// Readiness: can this instance serve traffic right now?
app.get("/readyz", async (_, res) => {
const checks = await Promise.allSettled([db.ping(), cache.ping()]);
const ok = checks.every((c) => c.status === "fulfilled");
res.status(ok ? 200 : 503).json({ ok, checks: checks.map((c) => c.status) });
});On a managed platform it is already there. Self-hosting, yes — Nginx or Caddy handles TLS, compression, static files and slow-client buffering far better than an application process should.
A load balancer distributes traffic across instances; a reverse proxy sits in front of servers handling TLS, caching and routing. Most modern tools do both, which is why the terms get mixed.
Derive it from measured throughput per instance and your p99 latency target under load, then add headroom for a failed instance and a deploy. Load test rather than guess.
A standard way to report server-side phase durations to the browser, visible in DevTools. Adding db, cache and render timings makes backend latency debuggable from the client.
ROVQIX Engineering
Engineering team, ROVQIX
The ROVQIX engineering team builds and maintains web platforms, APIs and infrastructure for clients across SaaS, ecommerce and enterprise. These notes come out of real production work — deploys, incidents, migrations and audits.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Microservices solve an organisational problem with a distributed systems bill. If you do not have the organisational problem, you just have the bill.
Every PostgreSQL connection is a process. Ten containers with a pool of twenty is two hundred processes, and your database was configured for one hundred.
Observability is not more dashboards. It is being able to answer a question you did not anticipate, without shipping new code.
No spam. Just the occasional case study and craft breakdown.