The table that was never least-cost
There are exactly two functions,NORMAL and LCR. A table that declares no function is NORMAL.
A third function,
RC, was removed in V1.28.0. It was in the enum, allowed by the database
constraint, and described in the panel — and nothing anywhere branched on it, so a table set to RC
routed as NORMAL while every surface an operator could consult said otherwise. Existing rows are
rewritten to NORMAL, which is what they were already doing.What changes
NORMAL table stops at the first matching rule, which makes file order the priority. An LCR table
scans every rule, treats the matches as alternatives rather than as a fallback chain, and lets only
the rate in rates.conf decide.
How a winner is chosen
1
Every rule in the table is scanned
No early exit. A rule whose target is another table is skipped with a warning — a nested cost
cannot be compared, and routing at an unknown price would be worse than not routing there.
2
Dead vendors drop out
A target that is down, suspended or otherwise not accepting is skipped, so the next cheapest
carries the traffic. This is the same signal a group uses; see
Vendors for the four states.
3
The survivors are priced
Each remaining candidate is rated for this message’s product against the cost side of
rates.conf. The cheapest wins.
4
An unrated candidate is the last resort
A vendor with no matching cost rate is kept aside and used only if nothing else matched, logging
No candidate in least-cost table … has a rate.A missing rate line degrades rather than blocks. Refusing to route an unrated message would turn a
forgotten line in
rates.conf into an outage; preferring it would send everything through whichever
vendor was least configured. Last-resort is the only answer that is neither.It is still worth alerting on, because a vendor that is winning traffic on a missing rate is winning it
at an unknown margin. See How pricing works.Two things refused outright
Both are refused at load, whole, because neither has a partial state that routes the way anyone intended — the table set stops updating until it is fixed.A vendor group inside an LCR table
A vendor group inside an LCR table
Least-cost picks by price and a group picks by policy. Expanding the members into rate candidates
would silently discard the policy, and a weighted group has no meaning to give a price comparison.Use one or the other in a given table: a group where you want failover and proportional splitting,
an LCR table where you want the cheapest live vendor.
A name that is both a group and a table, or both a group and a worker
A name that is both a group and a table, or both a group and a worker
Resolution is table, then group, then worker, so one of the two would be silently unreachable.
Rename one of them.
Copy-and-continue has no meaning here
+copied marks a rule as “add this target and keep scanning”. An LCR table already scans the whole
table and then chooses one cheapest candidate, so there is nothing for a copy to bypass. The panel says
so rather than letting the flag look meaningful.
Diagnosing a choice
outSms.routing.trace, set to one serial or one account id, explains which rules matched. The
selection itself — which candidate won and at what rate — is logged under outSms.routing.debug, which
is only usable on an idle gateway; see How routing works.
If an LCR table sends everything to one vendor, check in this order:
- Is the function name spelled
LCR? - Do the other candidates have a cost rate at all, for this message’s product?
- Are the other candidates accepting? A vendor that is bound but not transmittable is skipped.