Reliability
Keepalive, send windows, throttling, congestion, and automatic reconnect.
Everything on this page is what separates a demo from an SMPP integration that survives production: link supervision, backpressure, and recovery.
Keepalive and dead-link detection
TCP won't tell you an idle SMPP link died. Enable enquire_link keepalives:
new SmppClient({
enquireLink: 30_000, // shorthand for { intervalMs: 30_000 }
// or:
enquireLink: { intervalMs: 30_000, timeoutMs: 10_000, maxMissed: 2 },
});Unlike the original node-smpp — which sent pings but never noticed missing answers —
unanswered keepalives count as misses; after maxMissed (default 1) the connection is
declared dead, destroyed, and (if configured) reconnected.
Send window
SMSCs assume a bounded number of outstanding requests. Cap yours:
new SmppClient({
windowSize: 10, // max un-acked requests in flight
windowQueueLimit: 500, // optional: fail fast beyond this backlog
});Requests beyond the window queue FIFO and are released as responses arrive.
enquire_link bypasses the window so keepalives always flow, even under full load.
When windowQueueLimit is hit, requests reject immediately with
SmppWindowFullError — your signal to shed load upstream.
Throttling
When the SMSC answers ESME_RTHROTTLED or ESME_RMSGQFUL, submits retry with
exponential backoff (on by default):
new SmppClient({
throttle: { maxRetries: 3, baseDelayMs: 1_000, maxDelayMs: 60_000 },
// throttle: false → propagate throttling errors immediately
});
client.on("throttled", (status, delayMs) => {
metrics.increment("smpp.throttled");
});After the retries are exhausted the original SmppCommandError propagates.
Congestion
SMPP v5.0 peers report load via the congestionState TLV (0–100). The client tracks
it from every response:
client.congestionState; // last reported value
client.on("congestion", (value) => { // fires on change
if (value > 80) pauseCampaign();
});Automatic reconnect + rebind
Opt in and the client owns the whole recovery story:
const client = new SmppClient({
url: "smpp://user:pass@smsc.example.com",
reconnect: true,
// or: { maxRetries: Infinity, baseDelayMs: 1_000, maxDelayMs: 30_000, rebind: true }
});
client.on("reconnecting", (attempt, delayMs) => log.warn({ attempt, delayMs }));
client.on("reconnected", () => log.info("link restored"));On unexpected connection loss the client:
- emits
close, then schedules reconnect attempts with exponential backoff, - re-establishes the transport (TCP or TLS),
- re-binds with the last successful bind credentials (
rebind: true, default), - emits
reconnected(plus the usualbound).
Requests issued while the link is down wait for the reconnect instead of failing — in-flight requests at the moment of disconnect are rejected (they may have reached the peer; replaying them could double-send), but queued work resumes safely.
close() and destroy() never trigger reconnection — recovery only applies to
failures.
Observability
Wire the hooks to your logging/metrics stack; both default to no-ops:
new SmppClient({
logger: {
warn: (msg, meta) => log.warn(meta, msg),
error: (msg, meta) => log.error(meta, msg),
},
metrics: {
increment: (name, value = 1) => statsd.increment(`smpp.${name}`, value),
gauge: (name, value) => statsd.gauge(`smpp.${name}`, value),
},
});Counters include pdu.sent, pdu.received, request.timeout, request.throttled,
reconnect, enquire_link.missed; gauges include window.in_flight, window.queued
and congestion_state.