Writing
Three production bugs Sentry found in its first eight hours
October 8, 2026 · Amit Solanki
In August 2026 I put the first live demo of my restaurant ordering platform on a real server. The platform, now called Platestead, is a Rails and Spree application that takes a restaurant’s online orders, sends them to its point-of-sale system, and books delivery through DoorDash and Uber Direct. The demo served a demonstration restaurant, with no real merchant behind it, but it ran the way a production system does: its own server and database, scheduled background jobs, and live connections to outside services.
Three days after it went live, I added Sentry, an error-monitoring service that records every exception an application raises in production, including the ones no user ever sees. Within eight hours it had reported three bugs. None of them had shown any symptom: every deploy succeeded, every page loaded, every health check passed.
The three don’t share a root cause. What they share is more useful to notice: none of them was going to be caught by the tests a team normally writes, because each one only shows up once the code is running in its real environment. Two of them can’t be represented in a test at all.
Bug one: an email that was never going anywhere
Order #31 on the live demo tried to send its confirmation email. Spree::OrderMailer#confirm_email threw
Errno::ECONNREFUSED, connecting to localhost port 25.
The app’s own config/environments/production.rb already supports SMTP via environment
variables - a comment says plainly it “Works with any SMTP provider (Resend, Postmark, Mailgun,
SendGrid, SES, etc.)” That configuration block only activates when SMTP_HOST is present. It had
never been set. Not misconfigured - absent. ActionMailer’s real behavior when SMTP isn’t
explicitly configured isn’t an error at boot, or a warning in the logs, or a clear failure the
first time it’s exercised in development. It’s a silent fallback to Rails’ default mail delivery
settings - an SMTP connection to localhost:25 - which doesn’t exist on this box, or most boxes,
and produces a real exception only the instant something actually tries to send an email.
Every deploy, every health check, every page load looked completely fine. The one thing that could have surfaced this was an order actually trying to send an email, and nothing was watching when one did. No test suite would find this either, for a mundane reason: a test environment doesn’t read production’s environment variables, by design. The bug isn’t reachable from inside a test at all; it’s a fact about which environment variable was and wasn’t set on one specific server.
The fix is the boring kind: wire the real values through as deploy secrets. The part worth sitting with is what “boring” actually meant here. The demo had been live for three days before Sentry went in, and in all that time the backend could not have sent a single order confirmation. Nothing reported it until Sentry did.
Bug two: a background job that had never once succeeded
Sentry reported the same NoMethodError firing every hour, eight times in seven hours:
undefined method 'prunable' for class SolidCable::Message.
config/recurring.yml scheduled a cleanup task: SolidCable::Message.prunable.in_batches(of: 1000).delete_all, meant to prune old Action Cable messages past their retention window. Reading the
actual installed gem - solid_cable 4.0.2, its own app/models/solid_cable/message.rb - the scope
is called trimmable. It has never been called prunable, and 4.0.2 is the only version of the gem
this project has ever run. Claude wrote that line, early in the build. Later in the same build it
also wrote a permission grant broad enough to export the gift card table. Both are the
same failure: producing whatever best fits the surrounding shape. prunable is what a pruning scope
would be called, in a gem that has scopes like that. It is a plausible name. It has never been the
name. Nothing caught it, because nothing was watching a scheduled task’s own success or failure
until Sentry was.
The difference is where the two landed. The permission grant was catchable by reading the code,
and was caught by reading the code. This one was not. SolidCable::Message.prunable reads as
correct unless you go and open the installed gem to check, which nobody does for a cleanup task
nobody is watching. It needed production, running, being watched - which is this post’s own
argument, arriving a section early.
This is the purest version of the pattern: a recurring background job, running unattended, on a schedule, with zero user-facing surface at all. It doesn’t fail a request. It doesn’t show up in a browser. It doesn’t block a deploy. It just quietly throws, on its own schedule, forever, doing none of the work it was written to do - and the only way anyone would ever find out is something explicitly watching for exceptions that aren’t attached to any request a human is looking at.
A test could have caught this one, in principle: the test environment runs the same gem version as
production, so a spec that evaluated every command in recurring.yml would have raised the same
error. Nobody writes that spec for a cleanup task, and nobody had. The only thing in a position to
notice was something watching production.
Bug three: a library that fails on purpose, quietly, by design
The third one is the strangest, because the code doing the failing isn’t a mistake - it’s working exactly as its authors intended, and that’s the problem.
Spree::Addresses::GeocodeAddressJob fires on every checkout address create or update, calling
Geocoder’s default :nominatim provider to resolve latitude/longitude. Nominatim’s usage policy
requires a User-Agent header identifying the requester, and none was configured. Re-run against a
perfectly valid address, the lookup came back with “Geocoding API’s response was not valid JSON” and
returned nil: the service was refusing the request rather than answering it.
Here’s the part that makes this different from the first two bugs: Spree core already
anticipated geocoding could fail, and handled it - by catching the error internally and reporting
it via Rails.error.report instead of letting it raise. That’s a genuinely reasonable design
choice for a non-critical background enrichment step; a failed geocode shouldn’t break checkout.
It also means the failure was invisible to literally everything except something explicitly
subscribed to Rails’ own error-reporting stream. Verified directly against production: the job
had already run sixteen times, across the store’s 27 addresses, and exactly one of those runs showed
up in Sentry, because Sentry had only been live for a few hours. Re-running the lookup live failed
the same way, which is the only reason to think the other fifteen did too: nothing was watching them.
The fix wasn’t fixing the geocoding - it was noticing that nothing in the app actually reads
Spree::Address#latitude/#longitude anywhere. Real delivery-distance and serviceability logic
runs entirely through DoorDash Drive and Uber Direct’s own APIs, not Spree’s built-in geocoding.
The feature had no consumer. Rather than configure a User-Agent header for a lookup nobody
uses, the job gets disabled outright, via Spree’s own first-class Spree::Config[:geocode_addresses]
toggle - removing the vestigial feature rather than fixing a config value in service of nothing.
The actual lesson
These three don’t share a root cause, a language feature, or even a category of bug in the usual
sense - one’s a missing env var, one’s a misnamed method, one’s a working-as-designed error swallow.
What they share is where they live: outside anything the tests a team normally writes ever exercise.
A test doesn’t set production’s real environment variables. A test doesn’t run a recurring job on an
hourly production schedule for eight real hours. A test doesn’t exercise a third-party library’s own
internal Rails.error.report call against a live network dependency’s actual current policy.
Error monitoring doesn’t find the bugs your tests missed. It finds the bugs nobody was ever going to write a test for - because what’s wrong only shows itself in the gap between the code and the specific, real environment it’s actually running in. That gap doesn’t exist until deployment, and neither does any way to observe it, until something is watching production itself.
The checklist
- Any environment variable a config block only reads conditionally (
if ENV["X"].present?) deserves an explicit check that it’s actually set where it needs to be - the silent-fallback failure mode is exactly this shape, and it won’t show up until the feature gated on it is actually used for the first time in that environment. - A recurring/scheduled job succeeding at “not crashing the deploy” tells you nothing about whether it’s succeeding at its actual job. If nothing surfaces a scheduled task’s own exceptions, it can fail on every single run, forever, with zero visible symptom.
- When a library swallows its own errors instead of raising (often a reasonable design choice on
its own), that design choice also means your error monitoring is the only thing that can ever
tell you it’s failing - confirm you’re actually subscribed to whatever mechanism it reports
through (here,
Rails.error.report), not just assuming “no exception” means “no problem.” - Before fixing a background feature’s broken integration, check whether anything actually consumes its output. Sometimes the right fix is removing the feature, not repairing it.
- Wire error monitoring in before a feature ships, not after - the geocoding job had run sixteen times, and monitoring saw one of them.
Platestead itself, including what is and isn’t proven yet, is written up in the case study.
Working through something similar?