What Sentry Found: Observability, Stability, and the Shifting Cost of Watching Your Own Code
We turned Sentry back on, fixed the bugs it surfaced, and tuned the integration to live comfortably inside the free tier. Here is what the data told us — and what the observability market looks like in 2026.
Observability used to be the kind of thing engineering teams either over-invested in (an entire SRE org wiring up dashboards nobody read) or skipped entirely (you find out the service is down because a customer emails you). In 2026, neither extreme is acceptable. You need to know what is breaking — but you also need to know without paying enterprise sticker prices for an indie product.
This week we re-audited our Sentry integration, fixed the bugs it surfaced, and tuned the SDK to fit inside Sentry's free Developer tier. This post is a walkthrough of what we found, what we changed, and how the larger observability market is shaping our choices.
Why we went looking again
Our backend has been wired to Sentry since March. The DSN sits in Google Secret Manager, the secret is mounted into Cloud Run, and sentry_sdk.init runs on every startup. Email alerts were arriving. From the outside, it looked fine.
From the inside, it was not. The frontend was not wired to Sentry at all. Browser exceptions, hydration errors, and chunked-load failures were invisible. And the backend was sending Python errors into a project labelled javascript-nextjs, because that is the only project in the organisation. Stack traces were piling up in the wrong place, grouping was useless, and the dashboard was a stream of mixed-language events.
So we sat down with the data and worked through it.
What the data showed
We pulled the last ninety days of error events. A few patterns stood out:
A historical incident that fixed itself
The most-frequent error in the window was a Postgres StringDataRightTruncation — sixteen events in twenty-two seconds — coming from the social signup endpoint. The cause was a classic schema drift: the model definition allowed an 800-character bio, but the production database column was still VARCHAR(500) from an earlier migration. A user submitted a bio just over 500 characters, our Pydantic layer accepted it, and the INSERT exploded.
The migration to widen the column to 800 had already been written and deployed the next morning. But the root problem — that model and DB schema can drift without anyone noticing until a customer trips it — was still wide open.
We did two things. First, we tightened the request schema for social signup: explicit max_length on email, display name, and token so anything pathological returns 422 cleanly instead of reaching the database. Second, we extended our DB integrity script to compare information_schema.columns.character_maximum_length against the lengths the application expects. Now if a future migration shrinks a column under what the model can produce, the next deploy fails the integrity check.
A session that would not roll back
Further down the list, a sequence: one OperationalError: server closed the connection unexpectedly on POST /v1/upload, followed by a stream of PendingRollbackError from the same code path. Postgres had dropped the connection mid-query, the SQLAlchemy session was now in an invalid state, and the upload service tried to do partial-result persistence on top of it. Every subsequent operation in that session cascaded the failure.
The fix is small but easy to get wrong. When the upload pipeline catches an exception, the very first thing it now does is db.rollback() — wrapped in its own try/except so a failing rollback cannot mask the original error. After that, the partial-save logic runs on a clean session.
Logs that erased themselves
The WebSocket finish handler was logging err=%s against a caught exception. For most exceptions that prints something useful. For a handful of exception types whose __str__ returns an empty string, it printed err= with nothing after. We were getting Sentry events that told us a save had failed without saying why.
Switching to logger.exception(...) and err=%r cost nothing and made every future event self-describing.
Noise we did not want to pay for
The remaining items in the queue were not bugs. Uvloop occasionally raises RuntimeError: unable to perform operation on <TCPTransport closed=True> when a client disconnects mid-SSL. That is correct behaviour, not something we can fix, and on a free-tier quota of 5,000 errors per month it is wasted budget. We added a before_send filter that drops events matching those specific exception/value patterns. Same idea on the frontend for ResizeObserver loop limit exceeded and similar browser noise.
Tuning for the free tier
Sentry's Developer plan gives us 5,000 errors, 10,000 performance units, and 50 replays per month for free. That is plenty for our error budget — but only if you don't carelessly turn on every feature the SDK supports.
Our calibrations:
tracesSampleRate: 0on both backend and frontend. Distributed tracing is genuinely useful, but at 10K performance units per month we would burn through the budget on a single bad afternoon. We can turn it on with a tight sample rate when we need to investigate something specific.profilesSampleRate: 0on the backend. Same reasoning.- Session Replay disabled on the frontend. We left the integration code in place but set both sample rates to zero. If a critical bug needs replay context to debug, we can flip
replaysOnErrorSampleRateto0.05for a day and turn it back off. before_sendfilters on both runtimes to drop the events we already know are not actionable.
The result is a Sentry integration that captures real bugs without spending quota on noise — and without forcing an upgrade just because the dashboard hit its monthly cap.
What's changing in the observability market
We tuned for the free tier because Sentry's free tier is unusually generous for a hosted product, but it is worth understanding the broader picture, because the economics here are moving.
A few things have shifted in the last twelve to eighteen months:
- OpenTelemetry has finished winning. It is now table stakes that your SDK speak OTLP, your traces are vendor-neutral, and you can change backends without rewriting instrumentation. Sentry, Datadog, Honeycomb, and Grafana Cloud all support OTLP ingest. The lock-in moat is gone.
- Open-source alternatives have matured. GlitchTip is a Sentry-compatible error tracker you can self-host with a small Postgres database and a Django process. Sigtail (the open OTLP-native observability stack) covers traces, metrics, and logs in one place. For a one-room engineering team, the calculation of "host it yourself for $20/month of VM time" versus "pay $26/seat" is closer than it was.
- AI-assisted debugging is becoming a feature, not a product. Sentry now ships Seer, which proposes root causes and patches automatically. The MCP integration we used to write this post lets a coding assistant query Sentry the same way an engineer would. The product is no longer "we collect errors"; it is "we collect errors and then walk you through fixing them."
- Pricing pressure is finally real. Two years ago you priced observability by event count and accepted it. Today every vendor has a free tier, a usage-based mid-tier, and an enterprise tier that is mostly about SAML and audit logs. If you are a small team, the right play is to start free, instrument carefully, and only pay when you have demonstrated that the tooling pays for itself.
Our stance: we are staying on Sentry for now, because the free tier covers our actual needs and the developer experience around alerts and stack-trace grouping is still best-in-class. But we are instrumenting through their OpenTelemetry-compatible interfaces wherever possible, which means the day we decide to self-host (or move to a different vendor) is a configuration change, not a rewrite.
What is next
Two follow-ups for next week, both small:
- Create a dedicated Python Sentry project so backend errors stop being filed under
javascript-nextjs. The DSN gets swapped in the same Secret Manager entry the backend already reads from. - Wire the frontend DSN into Vercel's project settings so client-side errors start flowing in. The code is already there and conditionally-initialised — the only thing missing is the environment variable.
After that, we should be looking at every error in production with the right grouping, on the right project, with enough context to fix it — and without spending a dollar to do it.
Why this matters
When you cannot see what is breaking, you over-invest in defensive coding, or you under-invest and ship bugs. Both are expensive. Cheap, reliable observability lets a small team behave like a larger one: catch regressions before customers notice, fix them without guesswork, and spend the engineering hours on building new things.
That is the goal. The work this week pushed us a meaningful step closer to it.