Skip to main content

The edit that appeared to do nothing

A malformed rule fails the whole reload, and the previous table is retained. One bad line does not disable one rule — it rejects the entire table set, and the gateway carries on routing against the configuration it was already serving.Nothing looks wrong: traffic keeps flowing, the file on disk says what you meant, and the only trace is Error parsing new routing table at ERROR in the log. Check for it after every edit.
routingTable.conf is hot reloaded — an edit applies within about 200 ms, with no restart. That speed is exactly why the retained-table case is easy to miss: there is no restart to notice, so “my change did nothing” is the whole symptom.

What the router does with one message

1

The submission is answered first

The gateway sends submit_sm_resp, or the REST 200, before routing runs. The customer has already been told the message was accepted, and charged for it.
2

The message goes on the router queue

Accepted work waits here. See Queues and retries.
3

The `default` table is scanned

Rules are tried top to bottom and the first match wins. A rule may send to a vendor, to a vendor group, or into another table.
4

The chosen worker takes it

The message is enqueued on that vendor’s own queue and leaves at the speed the vendor allows.
Because step 1 happens before step 3, a routing failure cannot be reported to the client as a submit error. It can only surface later, as a delivery receipt and a row in cdr_rejected.

Tables are a graph, not a list

A rule whose target names another table chains into it, so default → MESSAGE → cheapest is an ordinary shape. Two things bound the recursion: A table reached twice by different paths is not a cycle: default → a → c alongside default → b → c loads normally.

The two things that load and then quietly misbehave

Both appear on /ops/health under routing_warnings, and each is logged once when it first appears rather than on every publish.
The catch-all belongs last. Placed above other rules it makes everything below it unreachable, and nothing refuses the table — the rules parse perfectly, they simply never run. A catch-all is a rule that narrows nothing: vendor1::default: in a file, or a rule with no conditions and no filters in the database.A rule carrying a filter is not a catch-all, however much it reads like one. See Filters.

When messages vanish

Before editing any rule, rule out the three causes that are not routing at all. Only reason = 'maxAttempts' in cdr_rejected means routing; headerNotApproved, templateNotMatched, missingMandatoryTlv, insufficientCredit and filtered all mean the message was refused before routing was reached. See Nothing is delivered.
These lines are at WARN or ERROR and need no debug flag:
routing.rule.broken is logged once per rule, not once per message, and the counter resets only when a new table is published. A quiet log is therefore not proof the problem is fixed — it is equally consistent with “already reported”.Watch the routing_rules_broken count on /ops/health instead, which is the same fact as a number.

Asking why one message did not match

Takes one message serial or one account id. Every rule that message is tested against then logs which condition failed and what the message actually held:
It applies live, so set it during the incident and clear it after. Empty — the default — costs one volatile read per rule.
This is not outSms.routing.debug. That one logs every rule of every message and renders the whole message object to do it, which makes it unusable at the rates where routing questions actually get asked. It is useful only on an idle gateway reproducing a single send.

Next

Writing rules

The four-part grammar, the operators that lie, and vendor groups.

Least-cost routing

Where rule order stops meaning anything.

Filters

Named conditions, and why disabling one narrows a rule to nothing.

Queues and retries

What is already paid for and sitting in memory.