Skip to main content
You have been through first checks and the rejection reason is maxAttempts. That is the one value that genuinely means routing.

The log names the cause

Each of these is at WARN or ERROR and needs no debug flag turned on.
routing.rule.broken is logged once per rule, not once per message, and the counter resets only when a new routing table is published.So after fixing a rule, watch for the line to reappear rather than for it to stay absent — a quiet log proves nothing. routing_rules_broken on /ops/health is the same fact as a number, and is the reliable way to tell “fixed” from “already reported”.

Two things to rule out before editing any rule

A perfect catch-all pointing at a disconnected vendor delivers nothing and looks exactly like a routing bug.
Answers in words: “N vendor(s) configured, none bound — nothing can be sent yet.”
A rule referencing one is skipped entirely rather than loaded with fewer conditions — because dropping conditions would silently widen the rule.Look for Skipping … filter '…' is missing or disabled. If that emptied the default table, the whole routing configuration is refused and you are in the ERROR case above.

Why a rule did not match

Six causes, and the last two account for most of the surprises:
  • Rules are evaluated top to bottom; a NORMAL table stops at the first match.
  • The fallback must be at the end. A catch-all above other rules makes everything below it unreachable — the panel’s routing page warns about both cases.
  • Attribute names must match those in the routing reference.
  • String comparisons are case-sensitive unless equalsIgnoreCase is used.
  • startsWith compares literal text. A regular expression written into it can never be true — use matches. The panel warns; a hand-edited file does not.
  • An unset field matches no positive operator, isNull excepted. product:equals:premium does not match a message carrying no product at all — but product:!equals:premium does.

Tracing one message

Set outSms.routing.trace to a single message serial or account id. Every rule that message is tested against then logs which condition failed and what the message actually held:
It applies live — set it during the incident, clear it after. Left empty it costs one volatile read per rule.
Do not reach for outSms.routing.debug. It logs every rule of every message and renders the whole message object to do it — thirty chained String.replace calls per line — so it is unusable at the rates where routing questions actually get asked. It is useful only on an idle gateway reproducing a single send.

The two queue numbers

How routing works

Tables, rules, and the order they resolve in.

Vendor rejections

When the vendor is bound and refusing every submit.