mepa8
Incident · Neon · 2026-10-07

Why our Neon bill reached $93 a month

A scheduler that never let the database sleep. Here is what the console showed, what the table was doing, and the one-statement fix.

By Ryan Bolden with NousPublished Updated

Neon is a serverless Postgres service that bills compute by the CU-hour and scales a database down, or to zero, when nothing is talking to it. That second part is the whole value. Our bill said it was not happening.

What we saw

On 2026-10-07 Ryan asked a six-word question: why is our Neon bill $100. The console answered. We were on the Scale plan, $24 had been charged for the month so far, and the run rate projected to between $93 and $106 by month end. The compute averaged 0.64 CU around the clock. Autoscaling was set to a range of 0.25 to 2 CU, and the database had never once gone idle long enough to scale to zero.

The database itself was 3,375 MB. For a CRM that serves phone lines for a handful of doctor's offices, that was 3 to 4 times what the live data needed.

At Neon's published Scale rate of $0.222 per CU-hour, a compute that never sleeps at 0.64 CU costs about $102 in a 30-day month before storage, which is priced at $0.35 per GB-month (neon.com/pricing). The math matched the bill. The question was why the compute stayed awake.

What was actually happening

One service in the CRM keeps a phone-index table in sync with the client list so caller ID can resolve a name. It ran every 5 seconds. Its statement was an INSERT … ON CONFLICT DO UPDATE SET customer_name = EXCLUDED.customer_name, updated_at = NOW() with no WHERE clause. Every run touched every row, whether or not anything had changed.

We measured it on 2026-10-08: 251 updates in 60 seconds on a table whose contents had not changed in days. Postgres writes a new row version for each of those updates, so the table had grown to 508 MB holding about 4 MB of live data. A second table, a trigger log with no retention rule, and four monitor tables nobody had written to since August made up the rest of the bloat.

Because a query arrived every 5 seconds, Neon's scale-to-zero timer never reached its threshold. The compute stayed at 0.5 to 0.64 CU for weeks.

Neon documents the behaviour plainly: scale to zero suspends a compute after a period of inactivity, and any connection resets that timer (Neon docs: scale to zero). A 5-second poller is the textbook way to defeat it.

What we changed

Three changes, in order of risk. First, the autoscaling ceiling went from 2 CU to 0.5 CU, with the floor left at 0.25. The CRM kept answering. Second, we backed up the heavy tables locally, dropped the four dead monitor tables, rebuilt the phone-index table (508 MB to 4 MB, a 1.9-second lock) and gave the trigger log a 30-day retention. The database went from 3,375 MB to 1,210 MB.

Third, the code fix. A pull request changed the sync to write only rows whose name actually changed, moved its cadence from 5 seconds to 5 minutes, and slowed a billing meter from every 5 seconds to every 60. It merged on 2026-10-08. It could not deploy, because the hosting platform had paused every deployment over an unpaid invoice. That story is its own incident.

So we put the fix in the database instead. A BEFORE UPDATE trigger on the phone-index table returns NULL when nothing but updated_at would change. Nothing in the repository reads that column; we checked. Measured again: 0 updates in 60 seconds. A real name change inside a rolled-back transaction still affected 1 row, so the trigger lets real work through. The rollback is one statement, DROP TRIGGER, written at the top of the function's comment.

What a monitor should have said

Two sentences, days apart, would have been enough. The first is the money. The second names the cause and the fix that was waiting.

“Neon (Postgres): on pace for $93.04/mo vs budget $50”Mepa8 alert, 2026-10-08 (table name shortened)
“phone_index churn 251 updates/min (fix merged in crm-block-theory PR #40 not deployed?)”Mepa8 alert, 2026-10-08 (table name shortened)

Mepa8 raises both today. The first comes from the plan rate times measured compute hours. The second comes from reading the table's update counter twice and dividing by the minutes between. Neither needs the vendor's cooperation.

The lesson

A serverless database only saves money if something lets it sleep, and the thing most likely to keep it awake is your own code. Any poller under a minute is a standing order to pay for compute 24 hours a day. The monitor that catches this is not a billing alert; by the time the bill moves, weeks have gone. It is a churn counter on the hot table and a budget line per vendor, read twice a day. Put the cost of a scheduler in the pull request that adds it. See the Neon vendor page for the figures Mepa8 watches.

Questions people ask

Why did Neon not scale to zero on its own?

Because a sync job connected every 5 seconds, and Neon resets its inactivity timer on every connection, as its scale-to-zero documentation describes. The compute stayed at 0.5 to 0.64 CU for weeks. The fix was in our code, not in Neon's settings.

Was the fix safe to run on a production database?

We think so, and we measured rather than assumed. Nothing in the repository reads the column the trigger skips, a real name change still affected 1 row inside a rolled-back transaction, and the rollback is a single DROP TRIGGER statement recorded in the function's comment.

How much will the bill be now?

The compute floor is 0.25 CU on the Scale plan, so the lowest practical figure is about 40 dollars a month, and the slower billing meter in the merged pull request should bring it near that once the deploy goes through. Moving to Neon's Launch plan would roughly halve the compute rate again.

Sources

  1. Neon pricing — Scale $0.222 per CU-hour, Launch $0.106, storage $0.35 per GB-month; fetched 2026-10-09
  2. Neon docs: scale to zero — inactivity timer behaviour
  3. Mepa8 vendor page: Neon — what the monitor reads

Change log: October 9, 2026 — First published. Numbers are from our own accounts and logs.