Observability · Internal platform

Knowing the platform is slow before a person walks over to say so

Every internal tool eventually reaches the point where "it's being slow today" is the only diagnostic anyone has. On a platform of almost two hundred routes carrying a warehouse floor, that isn't enough to act on. You can't tell a slow page from a slow database, a real outage from a routine permission denial, or a broken feature from one nobody uses.

12 observability endpoints
4 persisted telemetry tables
15s dashboard refresh cycle
Confidential client & operation

Built for the internal operations platform of a contract-logistics site, where one application serves floor screens, office reporting and a set of background automations from the same process.

Platform telemetry: latency percentiles, worker threads and a console on one screenA simulated telemetry dashboard with sample values. Four tiles across the top: active users, requests per minute, latency as p50 and p95, and server errors counted as 5xx only with client 4xx kept separate. Below them a latency trend over a one-hour range in five-minute buckets, with the p95 line showing the occasional spike the median hides, and a list of routes ranked by total wait, which is average duration multiplied by request count. At the bottom a worker-thread table says whether each thread is idle or busy, what it is running, where in the code and for how long, with the rule that the pool is replaced rather than pruned only when queue depth, tracked requests and live tasks all read zero, checked twice under a lock; beside it a console viewer over a ten-thousand-line ring buffer refreshing every two seconds. The note states the guard rails: capture fails open, the read path fails toward showing something, and the dashboard's own polling is excluded twice.ONE SCREEN · SIX TIME RANGES · ILLUSTRATIVE VALUESPlatform telemetry · 12 observability endpoints · 4 telemetry tables · 15 s refreshACTIVE USERS18distinct people in the rangeREQUESTS / MIN240page views apart from API callsLATENCY · P50 · P9585 ms · 410 mstail shown beside the medianSERVER ERRORS · 5XX ONLY3client 4xx counted separatelyLatency trend1 h range · 5-minute buckets, wider for longer rangesp95p501 h agonowusually fine, occasionally awful: only p95 shows itRoutes by total waitaverage duration × request count/receiving/scan/orders/list/audit/open/inventory/count/shipments/boardWorker threadsbusy threads say what, where in the code, and for how longTHREADSTATERUNNINGWHEREFORworker-1busybuild_pick_reportreports.pick2 sworker-2idle———worker-3busysync_orderssync.orders0.4 sworker-4idle———Pool replaced, not pruned: only when queue depth, tracked requests and live tasks all read zero,checked twice under a lock.Console viewerten-thousand-line ring buffer · 2 s refreshINFOsnapshot written, once a minuteINFOrequest GET /receiving/scan 200WARNqueue depth risingERRORflush failed: batch discardedINFOpool recycled after 10 idle minutesThe capture path fails open: a metrics error never alters a response. The read path fails toward showing something: in-memory ring buffers serve thedashboard during a database outage. And the dashboard's own polling is excluded twice, so it cannot become the most popular page in its own report.
The telemetry screen, with illustrative values: p50 and p95 latency rather than an average, routes ranked by total wait, what each worker thread is running, and the console viewer. Capture fails open; the read path fails toward showing something.

What does an internal platform need to report on itself?

An internal platform needs request timing, background-job outcomes, worker-thread activity and a log nobody has to remote in to read. This platform times every request, records each background call, rolls up a snapshot every minute, publishes Prometheus metrics, and shows p50 and p95 latency per route on one screen that refreshes every fifteen seconds.

The platform ran the site, and it ran blind. Console output scrolled past in a window on the server. The only latency figure anyone could quote was an impression. The way you found out a page was slow was that somebody walked over and told you.

That is survivable on a small tool. On one the whole floor depends on, it isn't — because the questions that matter can't be answered by looking. Which route is actually costing people time. Whether the worker pool is saturated or merely warm. Whether a background job failed or is still running. Whether the error someone reported was a fault or a permission check doing its job. Answering those is the groundwork for monitoring that pages a person instead of waiting for one to walk over.

  • Diagnosing a floor complaint meant remoting into the server to watch a console scroll
  • Nothing separated a slow page from a slow database call underneath it
  • A 404 and a 500 counted the same, so "error rate" meant nothing in particular
  • Thread-pool size was visible; what the threads were doing was not

What we built

Instrumentation at four levels, each answering a different question: profiling and request tracing built into the platform rather than bolted on. Every HTTP request is timed in an after-request hook and written to a per-request log with method, path, endpoint, status and duration. Every background function call carries a generated request identifier and records its module, duration and outcome. A snapshot thread rolls up active users, request rate, latency, error rate, live thread count and queue depth once a minute for the long view. Alongside those, a Prometheus exporter publishes active users, worker threads, queue depth and per-function timings and error counts, so external monitoring can scrape the platform without going through the dashboard.

On top of that sits one screen: six selectable time ranges from one hour to thirty days, fifteen panels and six charts across six sections, refreshing every fifteen seconds. It is organised by question rather than by data source — real-world performance, how people actually use the app, where to spend engineering effort, usage patterns over the day and week, error intelligence, and a collapsed section holding the infrastructure detail that only matters when something is wrong.

  • Latency reported as p50 and p95 — overall, per route, and per time bucket
  • Trend bucket width scales with the range, from five minutes up to a day
  • Human page views separated from API and background traffic by method and path
  • Client 4xx and server 5xx counted separately; the headline rate is 5xx only
  • Standard output and error are wrapped at process start and tee'd to a rolling ten-thousand-line memory buffer and a size-rotated file, so a print statement anywhere in the codebase becomes a levelled log line the viewer can filter
  • A console viewer with level filtering, two-second auto-refresh, download and buffer clear
  • A health indicator driven by server error rate and worker saturation rather than uptime

How do you monitor an app without slowing it down?

A dashboard that polls itself every fifteen seconds will, within a day, be the most popular page in its own report. Telemetry paths, static assets, images and the metrics endpoint are excluded twice — once at the point of capture and again in the SQL fragment shared by every aggregate query — specifically so the two definitions can't drift apart and quietly reintroduce the noise.

The capture path fails open. The after-request hook is wrapped so that any exception is swallowed and the response leaves unchanged, and the persistence call inside it carries its own separate guard. Records are handed to an in-process queue and drained by a dedicated writer thread that flushes on ten records or two seconds, whichever comes first. A failed flush discards the batch in a finally rather than retrying it, and an unexpected error in the writer loop itself sleeps five seconds before continuing, so a database outage can't become a hot loop. Losing a metric is acceptable. Slowing down someone scanning freight is not.

The read path fails the other way — toward still showing something. Aggregates are computed in the database over the selected range, but the process also keeps in-memory ring buffers of recent timings, process records and errors. If the database read throws, the endpoint rebuilds the same breakdown structures from those buffers and serves them, so the dashboard keeps working during exactly the outage you'd most want to look at it during.

  • The percentile, bottleneck and latency-trend queries are each guarded individually, so one unsupported query degrades a single panel instead of the page
  • Timestamps are explicitly marked as UTC, because a browser silently reading them as local time makes a dashboard lie rather than fail
  • Logging errors are written to the real error stream, not back through the wrapper, so a failing logger can't recurse
  • The metrics exporter is initialised at the bottom of the module, after every route is defined, because it instruments the view functions that exist at the moment it runs

Recycling a Python thread pool you cannot kill

Python won't let you kill a thread, and a thread pool keeps its workers alive once it has grown. After a burst the pool climbs to its ceiling and stays there for the rest of the process lifetime — which on a graph looks identical to a platform that is permanently busy.

So the pool is replaced rather than pruned: build a new executor, swap it in under a lock, shut the old one down without waiting. What makes that safe is the refusal to do it at the wrong moment. Queue depth, tracked requests and live task activity must all read zero, and the check is made twice — once by the endpoint, which returns a conflict and a state dump instead of acting, and again inside the swap itself while holding the lock. A maintenance loop runs the same routine every minute and only recycles after ten unbroken minutes of idleness.

Knowing which workers are actually working needed its own mechanism. Task submission is wrapped so each job registers its thread, the callable it is running and its start time in a lock-guarded table, and removes the entry in a finally block. The dashboard reads that table alongside live interpreter frames, filters the stack down to frames from the application's own source tree, keeps the last ten and shows the deepest as the thread's current location. Idle threads say idle. Busy threads say what they are busy with, where in the code, and for how long.

The result

The questions that used to need a person now have a screen. Which routes cost users the most total waiting time — ranked by average duration multiplied by request count, so effort goes where the volume is rather than where the single worst case is. Which routes are usually fine and occasionally awful, which only a per-route 95th percentile exposes. Which parts of the platform people actually reach, how many distinct people reach them, and at what hour of the day.

The unglamorous half was deciding what not to count. Excluding the dashboard's own polling, separating a routine permission denial from a genuine fault, reporting a median beside a tail instead of an average that hides both. None of it is difficult, and all of it is the difference between a dashboard people act on and one they stop opening. Those are the questions upkeep and uptime monitoring for the apps we build has to answer too.

Questions the platform could not answer, and now can

QuestionBeforeAfter
Which route costs users the most waiting?An impressionRanked by average duration times request count
Was that error a fault or a permission check?A 404 and a 500 counted the sameClient 4xx and server 5xx counted separately
What are the worker threads doing?Pool size onlyEach busy thread's job, place in the code and run time
What did the console say?Remote into the server and watch it scrollA viewer with level filtering, download and two-second refresh

Questions about this build

Why report p95 latency per route instead of an average?

An average hides both the typical request and the bad one. A median beside a per-route 95th percentile shows which routes are usually fine and occasionally awful, and ranking routes by average duration multiplied by request count puts engineering effort where people wait longest in total, rather than where the single worst request happened to land.

What happens to the monitoring when the database is down?

Capture fails open: the after-request hook swallows its own exceptions, records go to an in-process queue, and a failed flush discards the batch rather than retrying, so an outage cannot slow anyone scanning freight. Reads fail the other way: the process keeps in-memory ring buffers of recent timings and errors, and the dashboard rebuilds its breakdowns from them when the database read throws.

Built with

  • Python
  • Flask
  • SQL Server
  • Prometheus
  • Chart.js
  • Thread pools & work queues

On the details we left out

Client, location, system names and volumes are withheld deliberately. We're happy to go deeper on architecture and approach on a call.

Services this draws on

Workload Optimization

Squeeze more throughput out of the hardware and licenses you already pay for.

See Workload Optimization →

Custom Software & Web Development

Websites, internal tools, APIs, and automation for teams without a dev department.

See Custom Software & Web Development →

More projects

Release engineering · Change visibility

Deploying to a system that nobody can stop using

A platform the whole site works on can't have a maintenance window. We built a two-slot deployment pipeline that refuses to stop the running version until the new one is proven up — plus an in-app changelog and a request board so users can see what changed and what's coming.

Read the story →
Operations platform · Built for ourselves

One app that runs the whole company, and keeps score

The Do-Engine is the operations app we built to run our own business: a task board, a client book, a document vault, invoicing, a help desk and a mail client in one page, with a single XP ledger scoring everything that gets finished.

Read the story →
Platform · Multi-year build

One application that replaced a decade of spreadsheets

A warehouse ran its floor on shared spreadsheets, emailed reports, and tribal knowledge. We replaced all of it with a single internal platform — and kept it running while people worked.

Read the story →
Get in touch

Is 'it's slow today' your only diagnostic?

Name the app and the screen people complain about. Free assessment.

Prefer to talk?

Call 832-598-8234 or email msco@stoneagesoftware.com. Houston, Texas — serving Houston, The Woodlands, Conroe, Sugar Land, Katy, Pearland, and the Greater Houston metro.

What happens next

We read your message, an engineer replies with questions or a straight answer, and if a call would help, we book one. From the first reply on, you're talking to the people who'd do the work.

No obligation — we reply within one business day. This form sends your details to Stone Age Software LLC so we can respond to you. See our privacy policy for how we handle them. This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.