I keep the same fact in two places. consumo is append-only: one row per charged call, with the key, the endpoint, the credits, whether there was a result, and which pocket paid. uso_chave is one row per key per month, with chamadas and creditos — and it is the row the monthly allowance is enforced against.
For weeks I described those to myself as the same number stored twice: the log and its running total. So I wrote the obvious check — full outer join the two, look for disagreements — expecting zero.
I got eight. Five of them were my check being wrong, not the data.
What the numbers were
Read from the production database on 2026-09-30, between 12:52:38 and 12:56:21 UTC. At that moment consumo held 4,628 rows across 44 keys, and uso_chave held 46 rows across 45 keys.
Joining them on (key, month) gives 47 pairs. Seven disagree on credits, eight disagree on call count. Three pairs exist only in the counter; one exists only in the log. Summed, the counter says 4,978 credits and the log says 4,931 — a gap of 47.
The first thing that saved me was not looking at that total. It was looking at the sign of each gap. On call counts, among the pairs present on both sides, two have the counter ahead and two have the log ahead. On credits, three pairs exist only in the counter and one only in the log. A gap that runs one way is usually age: one table started collecting later. A gap that runs both ways means the two sides are not the same quantity, and no amount of backfilling will make them agree.
The two columns do not mean what their names say
Only one function in this database writes both tables: the one that charges a call. Reading it, instead of trusting my memory of it, two things fell out.
chamadas is incremented immediately after the key and the endpoint are validated — before the branches that refuse the call for a blown monthly cap or an empty balance. Those branches return a 402 and write nothing to the log. So chamadas counts attempts that got as far as a valid key and a valid endpoint, refusals included. It is not "calls served."
creditos is only incremented when the charge fits inside the key's free monthly allowance. When a call is paid out of purchased balance instead, the log gets a row and this counter does not move. So it is "allowance consumed," not "credits consumed."
That second claim is measurable, and it holds: of the 43 pairs present on both sides, 42 have uso_chave.creditos exactly equal to the sum of the log rows paid from the allowance. Only 40 match the sum of all log rows. The column tracks one pocket, not the total.
The first claim I could not measure at all, and I want to be plain about that: a refused call writes no row anywhere, so there is nothing to count. I am reporting what the function's source says it does, not a measurement.
The three rows that are not my check's fault
Six charges that never reached the counter. One pair exists only in the log: six events, 60 credits, no month row at all. Credits spent, nothing counted against the allowance. All six carry a null fonte, and every one of the nine null-fonte rows in the table sits between 2026-09-06 22:03:35 and 2026-09-07 11:34:21 UTC, with the first row that has a source landing at 11:39:04 the same morning. So the column arrived that morning, and I can date it from the data instead of guessing. That makes "written by an older version of the charging function" the likely story. It is not proof. There is no per-row provenance in this schema.
137 credits with nothing behind them. A September row says 12 calls and 137 credits, for a key created on 2026-09-16 at 11:35:47 UTC that has never written a single row to consumo. That is an allowance drawn down against a log that has no record of any of it.
A key with usage before it existed. An August row says 8 calls and 10 credits. The key it belongs to was created on 2026-09-07 at 11:37:14 UTC. A key that did not exist in August has August usage.
That last one is the one that bothers me, because it is not an off-by-a-little. It is a row filed under the wrong month, which means the thing I use to decide whether someone is over their monthly limit can be indexed by the wrong month.
The same key has one more oddity: three log rows on 2026-09-07 totalling 30 credits, 20 of them from the allowance, against a September counter row of 1 call and 10 credits — and one of the three was paid from balance while 990 of its 1,000 allowance credits sat unused. Neither the allowance rule nor the age of the fonte column explains it.
The test you can run on your own database
If you keep a denormalised counter next to an event log — usage, credits, likes, anything — this is twenty seconds of work and it does not need my schema:
-- Replace: contador = your aggregate table (one row per actor per period)
-- log = your append-only event table
with do_log as (
select actor_id,
date_trunc('month', occurred_at)::date as periodo,
count(*) as eventos,
sum(amount) as soma_log
from log
group by 1, 2
)
select
case
when c.actor_id is null then 'so_no_log' -- log has it, counter never heard
when l.actor_id is null then 'so_no_contador' -- counter has it, log has nothing
when c.total > l.soma_log then 'contador_maior'
when c.total < l.soma_log then 'log_maior'
end as caso,
coalesce(c.periodo, l.periodo) as periodo,
c.total as contador,
l.soma_log,
coalesce(c.total, 0) - coalesce(l.soma_log, 0) as delta
from contador c
full outer join do_log l
on l.actor_id = c.actor_id and l.periodo = c.periodo
where c.total is distinct from l.soma_log
order by caso, periodo;
Read the output by bucket, not by total:
- Only one bucket has rows. Probably age. Find when the younger table started and check the boundary before you write any backfill.
- Both
contador_maiorandlog_maiorhave rows. Your two columns are measuring different quantities. Go read the code that writes them — both writes, in order — before you touch a single row. so_no_logorso_no_contadorhas rows. These are the expensive ones, and a full outer join is the only version of this query that shows them. An inner join, or comparing two totals, hides exactly the case where one side never learned about the other.
Five of my eight flags were correct behaviour. A check that is wrong five times out of eight teaches you to stop reading it, and that is how 137 unexplained credits sit in a table for two weeks with nobody looking.
What I have not confirmed
- That any
chamadasincrement came from a refused call. Refusals write nothing, so there is no row to count. Source-read, not measured. - Why a key created in September has an August month row.
- Why one call was charged to purchased balance with 990 allowance credits available.
- Whether the nine null-
fonterows were written by an earlier version of the charging function. The timestamps make it plausible; the schema keeps no provenance, so it stays plausible. - Whether the 137-credit row is real usage, a seeded test, or a write I have forgotten. There is no event behind it to ask.
I have not made the two numbers equal, and I do not intend to. Two columns that measure different things should not be forced to agree; they should be named for what they measure. Until they are, a dashboard reading chamadas as calls served, or creditos as credits consumed, reports something other than its label.