Case study·Public
Error Tracking Platform
A minimal Sentry, rebuilt
- Ingest
- Ack from Redis
- Storage
- PostgreSQL
- Auth
- Per-project API keys
- Runs as
- docker compose
I built this to understand how Sentry works from the inside: what happens between an exception being thrown in someone's app and it showing up, grouped and searchable, on a dashboard. It's a Go/Chi API, Redis, PostgreSQL, and a Next.js dashboard, all started with one docker compose up.
Ack first, persist second
An error reporter runs inside someone else's app, often while that app is already failing. So ingest has to be fast and boring. The API checks the X-API-Key, pushes the event onto a Redis queue, and answers straight away. A background worker drains the queue into PostgreSQL at its own pace.
This means a burst of errors (a bad deploy that throws on every request, say) lands on Redis instead of turning into a burst of Postgres writes. It also means the API instances are stateless, and docker compose up --scale backend=3 just works.
What that costs
Acking before persisting means there's a window where an event exists only in Redis. If Redis loses it before the worker drains it, it's gone. That's why Redis persistence settings matter here in a way they wouldn't for a pure cache. For error reports, losing a few duplicates of a crash during an outage is a better failure than the reporter slowing down the app it's reporting on. For payments it wouldn't be.
The data model
Each event is a row in errors: level, message, stack trace, source (frontend, backend, api), URL, a JSON context blob for whatever the caller wants to attach, a resolved flag, and a fingerprint. The API covers creating, listing with level/source filters, fetching one event, resolving, deleting, and aggregate stats.
Reads
Dashboard reads go through the API, which serves from a Redis cache with a TTL before falling back to PostgreSQL. Triage views are read far more often than they change, so a short TTL takes most of the load off the database. The cost is that the dashboard can briefly show slightly stale data.
What I'd build next
- Grouping by fingerprint. The column is there, but events aren't collapsed into issues yet. That grouping is most of what makes Sentry usable at volume.
- Rate limiting per key, so one runaway client can't fill the queue for everyone.
- Push instead of poll: a WebSocket feed of new events for the dashboard.
- Source maps, to turn minified frontend stack traces back into something readable.