The design in one paragraph
An account’s spendable balance is held in memory and decremented per message, so the message path does no database work at all. Durability comes from reserving in blocks: one atomic statement moves an amount frombalance into reserved, and the gateway spends that block from memory.
At a few thousand messages a second that is a couple of database writes per second rather than a
couple of thousand — and no message is ever sent against credit that was not committed first.
The identity that must hold
kill -9 strands
credit in reserved; it does not lose it — POST /ops/credit/release returns it, and a clean
shutdown returns it automatically.
If the two sides disagree, that is a real discrepancy and not a rounding artefact.
Topping up
Credit is added by writing aRECHARGE row. The gateway applies it on its next poll and marks it
applied, so a replay is a no-op.
500000 is 50.0000 units.
The control panel’s Add credit writes exactly this row and nothing else. The gateway itself
cannot create credit — it only applies rows somebody else wrote, and the balance column has
exactly one writer, which is this poll.
Block size, and the refusal it explains
A block is sized from the rate the account has been observed sending at. From cold — or after an idle spell — there is no rate to size it from, so a floor applies:smsg.balance.block.min.messages defaults to 20, and the price comes from the message asking for
the reservation.
A reservation is all-or-nothing, so a single statement can only succeed if the balance covers the
amount asked for. The reserve therefore tries three descending rungs — the full block, the
message-denominated floor, then just the one message that asked:
What it costs: an actively sending account holds up to twenty messages’ worth in reserved that
nothing else can spend — returned by a clean shutdown, and by credit release after an unclean one.
What you will still see near the end: refills are asynchronous by design, so a message arriving
while the next reservation is in flight can be refused even though credit remains. That refusal is
transient and the retry succeeds. It is not stranded credit.
Refusal
An account with no credit getssubmit_sm_resp status 0x402, HTTP 402, reason
insufficientCredit.
ESME_RTHROTTLED is deliberately not used: it means something else, and would make a client back off
and retry a condition that only a top-up can clear.
Refunds
A message charged and then dropped during routing has its credit returned exactly once. An unrated message is never charged at all. Refill triggers on a watermark while credit remains, on a background thread. If traffic outruns it the message is refused immediately rather than the SMPP thread waiting on a database — a stalled ingress thread is worse than a refused message.Properties
Related
Postpaid
Credit limits, and exact bills from approximate enforcement.
Refusals
Reading
insufficientCredit against a positive balance.