Skip to main content

SMPP production lifecycle

Connect via SMPP gets a bind on the wire and one message sent; the wire-level recipes show the exact bytes. This page is the layer after the first successful submit_sm: the decisions and discipline that keep a bind healthy at 2 a.m. — which dlrMode to run, how to keep the session alive and re-establish it safely, how much to push through one bind, and the symptom-to-fix table for the failures an aggregator on-call actually sees.

1. Recap: bind and credentials in one paragraph

Create a credential with POST /api/v1/messaging/smpp/credentials (or Developer → SMPP in the dashboard), copy the system_id and the one-time 8-character password, and bind a transceiver to smpp.orbit.devotel.io on 2775 (plain) or 3550 (TLS). The full walkthrough, runnable Python and Node clients, ports, and the diagnostic ladder live in Connect via SMPP; the byte-level bind and submit listings live in SMPP wire-level recipes. This guide assumes a bind that works and covers what changes when it must keep working. Two facts from that page carry most of the weight here:
  • One credential supports one live session at a time. A second bind while the first is alive is rejected with ESME_RALYBND. Scaling out means more credentials, not more binds per credential.
  • Every submit_sm passes the same compliance gates (opt-out suppression, quiet hours, consent) as the REST path. A gate rejection arrives as a nonzero command_status in submit_sm_resp, never as a silent drop.

2. Choose a DLR delivery mode

The dlrMode field on the credential decides where each delivery receipt leaves Orbit: Which one to run: Switching is a PATCH on the credential row and propagates within one reconciliation pass (about 30 seconds); receipts generated before the change keep the mode in effect at generation time. If a termination rule moves your traffic off the SMPP ingress (the absorbed-traffic case in the receipt window guide), the rule can require dlrMode webhook or both (require_dlr_mode) so a bind-only credential never ends up as a receipt black hole.

The deliver_sm PDU and its webhook twin

A receipt on the bind is a deliver_sm whose message_id is the exact string your submit_sm_resp returned — that id is the join between your operation table and the delivery world:
Parse stat: from short_message and join on message_id. On absorbed traffic (say, SMPP submits terminated onto WhatsApp) the same envelope carries the canonical Orbit failure vocabulary in err: — 81 (not on destination channel), 88 (re-engagement window lapsed), 89 (rate limit), 00 (unspecified provider error) — per Send-side DLR model. With dlrMode webhook or both, the same receipt is POSTed as a lifecycle event carrying the same canonical status, so one table maps the two planes: Two asymmetries to code around: read has no SMPP stat (DELIVRD already fired on the bind), and submitted_no_receipt is a webhook-plane status with no bind-side stat. The bind and webhook encodings are otherwise identical by construction, per SMPP edge model.

3. Long-lived bind discipline

One transceiver per process, one process per credential. A transceiver carries sends and receipts on one session, which is what almost every integration wants. Split a receiver-only bind out into its own process only when you deliberately scale receipt consumption separately — and give that process its own credential, because ESME_RALYBND means two processes can never share one. The discipline that keeps the session alive:
  • Keepalive. Send enquire_link every 30–60 seconds and treat a missing enquire_link_resp after a 10-second deadline as a dead bind. Silence longer than the edge’s idle window (30 minutes by default) gets the session torn down server-side.
  • Reconnect with backoff and jitter. After any drop, rebind on an exponential schedule with full jitter — for example a 1 s base doubling to a 60 s cap, delay = random(0, min(cap, base * 2^attempt)), resetting attempt on a successful bind. A rapid rebind loop is the bind-abuse pattern every SMPP server throttles and flags; jitter also stops a fleet of clients from re-binding in lockstep after an edge restart.
  • Drain before unbind. On planned shutdown, stop enqueueing, wait for in-flight submit_sm_resp and pending deliver_sm, then unbind. Receipts in flight across a TCP close do not re-queue.
  • Budget the reconciler. Credential changes — rotate, suspend, dlrMode, tpsLimit, allowedCidrs — take effect within one reconciliation pass (about 30 seconds). Rotating a live credential invalidates the old password immediately and drops the live bind; schedule it in a maintenance window.
  • The receipt timeout window. When your submits are absorbed onto another channel, the UNDELIV you fail over on fires at the rule’s receipt_timeout_seconds (10–120 s, default 30). Set it against your measured confirmation curve, not the default — SMPP: the no-delivery receipt window is the how-to.

Detecting a zombie bind

A zombie bind is a session that your monitoring believes is dead but that still holds the credential’s single slot — a half-killed replica, a client whose TCP connection is open but whose read loop is stuck. Three signals expose it:
  1. ESME_RALYBND on (re)bind while “nothing else is connected.” Something holds the slot. This is the unambiguous one.
  2. Your side is silent but the dashboard disagrees. The reconciliation loop stamps each credential’s live bind count and status back onto your row about every 30 seconds, and Developer → SMPP → Credentials renders it. If every one of your clients is down but the credential still shows a live bind, a zombie holds it.
  3. Your own keepalive telemetry. Track enquire_link_resp misses and submit_sm_resp rates per bind in your metrics; a TCP connection that is open but unanswered is a zombie on your side of the wire, even before the edge notices.
The operator escape hatch when a zombie holds the slot: suspend the credential (or rotate it). Existing sessions drop within one reconciliation pass; re-activate and bind fresh. If the zombie is yours, also fix the shutdown path — a process that exits without unbind leaves the slot occupied until the edge’s idle teardown.

4. Throughput tuning

Four knobs, in the order they bind:
  • The credential’s tpsLimit is the contract — 50 submissions per second by default, adjustable per organization up to the platform ceiling. The cap counts per second per bind on a sliding window: a burst in one second doesn’t taint the next, and exceeding it answers submit_sm_resp with ESME_RTHROTTLED.
  • The window is your pacing mechanism. SMPP has no batch PDU; batching is pipelining — many submit_sm in flight over one bind, drained by their responses. Keep a window of unacknowled submits (start in the low tens), and raise it only while submit_sm_resp latency stays flat and no ESME_RTHROTTLED / ESME_RMSGQFUL appears. A full window means stop enqueueing, not drop.
  • Ramp, don’t blast. On a fresh bind or after a reconnect, ramp your enqueue rate over the first minutes instead of opening at full rate; a cold path absorbs less than a warm one, and ESME_RMSGQFUL is the signal you didn’t.
  • One bind has a practical ceiling. If a raised tpsLimit still binds your throughput, open several binds (several credentials) and spread load — that is the supported scale-out path, not pushing one session past its limit.
The response-latency budget to hold yourself to: If submit_sm_resp p99 climbs toward your deadline while the send rate holds, the window or the upstream path is saturating — back the enqueue rate off before the edge does it for you.

5. Failure runbook

Work the matching section, not the whole page.

Bind rejected

Symptom: bind_transceiver_resp returns a nonzero command_status. A suspended credential also rejects new binds and drops live ones within a reconciliation pass — if a bind that worked an hour ago now fails, check the credential status before the network.

submit_sm_resp with nonzero command_status

Symptom: The bind is healthy; individual submits are refused.

Unacknowledged submit_sm flood

Symptom: The window fills with submits that get no submit_sm_resp; enqueue stalls.
  1. Stop enqueueing immediately; the window is your brake.
  2. Read the socket. A client that stopped reading (backpressure bug) sits on a healthy connection with a full window; draining responses and deliver_sm can unstick it without a rebind.
  3. Probe with enquire_link. A response proves the session alive — keep draining. No response inside 10 s means the TCP path is half-dead: rebind with the backoff schedule above.
  4. Reconcile, don’t blind-resend. Every submit that never got a response is in “unknown”: retrying risks duplicates. Record what you have (sequence number, and message_id for everything that did ack) and reconcile against the webhook plane or your message history — the statuses eventually land as delivered, undelivered, or expired events.

DLR loss

Symptom: submit_sm_resp said accepted; no receipt arrives inside your window plus margin.
  • dlrMode is bind and your client was down at receipt time. Receipts queue for the next bind. Bind again and drain, or move the credential to webhook/both if client downtime is a normal state for you.
  • dlrMode is webhook and the endpoint is failing. Delivery retries run for about 4.3 hours before the event dead-letters; check your endpoint’s 2xx rate and the DLQ, per Build a DLR-only webhook consumer and Webhook delivery semantics.
  • Absorbed traffic. The synthesized UNDELIV fires at receipt_timeout_seconds, not at carrier speed — a quiet bind on absorbed submits is often just the window still running.
  • The handset never receipts. Some MNOs confirm slowly or never; the row closes expired late. The no-delivery receipt window and submitted-no-receipt cover the tuning and the diagnosis.

6. Observability

Where to look first. Developer → SMPP is the per-system_id console: the Credentials tab shows each credential’s live bind state and count (stamped by the reconciliation loop about every 30 seconds) beside the connection details; the Carriers tab shows each upstream connector’s bindStatus (BOUND / UNBOUND / BIND_FAILED) and lastSeenAt; the Compliance tab shows how the tenant gates apply to this wire. Per-message, the message row’s status timeline shows how long confirmations actually take on each channel — the curve you tune the receipt window against. What to alert on, on your side: Handing a capture to support. On the SMPP wire the tool is tcpdump (sngrep belongs to SIP legs elsewhere in a CPaaS stack, not this one):
On 3550 the capture shows framing and timing but not payload, since the session runs inside TLS — pair it with your client’s PDU log (the logging.DEBUG trace in the Python example, the pdu event in the Node one). Send support the capture, your system_id (never the password), the client library and version, and the failing time window with timezone; the sequence numbers in the capture line the session up against the edge’s view.

See also