BroadNet
The JournalProduct
Product

SMPP window size: why your bind is slower than the rate you pay for

An operator agrees 100 messages per second, the bind delivers twelve, and everyone blames the network. Nine times out of ten the cause is a window left at its default. Here is what windowing really does on an SMPP session, and how to size it.

BroadNet Engineering
11 min read
Product · Issue 05
11 min read

pattern we see constantly: an enterprise signs for 100 SMS per second, points its application at the bind, and measures twelve. Bandwidth is fine. The SMSC is fine. Nobody has been throttled. The traffic is simply crawling, and the first instinct is always to blame the route. It is almost never the route. It is the window.

SMPP is an asynchronous protocol that most integrations accidentally run synchronously. Understanding why is the difference between paying for throughput and receiving it.

What the window actually is

The window is the number of PDUs you may have submitted but not yet had acknowledged, on a single bind, at any one moment. Send a submit_sm and the SMSC replies with submit_sm_resp — but SMPP never required you to wait for that reply before sending the next message. The window is how far ahead you are allowed to run.

With a window of 1, you submit, you wait, you receive, you submit again. Your throughput is now bounded by the round-trip time, not by anything you agreed commercially. At 40 ms round trip that is 25 messages per second — no matter what your contract says. With a window of 10 you have ten in flight at once and the same link carries roughly ten times the traffic over the identical connection.

Window size is the single biggest determinant of the throughput you get from a bind, and it is the setting most often left at a library default that silently caps an integration far below its agreed rate.

Why so many libraries default to one

A window of 1 is the safe default for a library author. It needs no correlation state, no timeout bookkeeping, and no reordering: every request has exactly one outstanding response, so a naive client can treat SMPP as request/response and never notice the difference. It works, it is easy to reason about, and it is slow. Many SMPP client libraries ship this way, and the value never gets revisited because nothing appears broken — the messages do arrive, just not quickly.

Sequence numbers are what make a window possible

Every SMPP PDU carries a sequence_number in its 16-octet header. When several submissions are in flight simultaneously, that number is the only thing tying each submit_sm_resp back to the submit_sm that caused it. Responses are not guaranteed to arrive in the order the requests were sent, so a client running a window larger than 1 must keep a map of outstanding sequence numbers and match on arrival.

Two rules matter. Never reuse a sequence number while it is still in flight — a late response to a recycled number will be attributed to the wrong message. And always time out entries: if a response never arrives, the slot must be released or the effective window shrinks to zero over the life of the session, which produces the maddening symptom of a bind that starts fast and degrades over hours.

You can watch this happen field by field in our SMPP session simulator — it shows the sequence_number in the header of every PDU, and which response is answering which request.

Window size and TPS are different limits

These get conflated constantly. TPS is a rate limit the operator enforces: submissions per second, measured over a window of time. The SMPP window is a concurrency limit you configure: PDUs outstanding at an instant. They interact but they are not the same number.

The relationship is roughly: achievable TPS ≈ window ÷ round-trip time. A window of 10 with a 50 ms round trip gives about 200 TPS. If the operator caps you at 100, you will hit the cap and receive ESME_RTHROTTLED on the excess. If the operator allows 500 and your window is 10, you will never see 500 no matter how much traffic you offer — you are concurrency-bound, not rate-bound.

WindowRound tripCeilingSymptom
140 ms~25 TPSContracted rate never approached; link looks idle
1040 ms~250 TPSHealthy for most enterprise senders
5040 ms~1,250 TPSOnly if the operator agreed it; expect throttling otherwise
10250 ms~40 TPSLatency-bound — raise the window or move closer

Reading ESME_RTHROTTLED correctly

ESME_RTHROTTLED (0x00000058) means you exceeded the agreed submission rate. It is not a rejection of the message and it is not a routing failure. The correct response is to back off and retry that message — treating it as permanent silently drops traffic that the operator would have accepted a moment later.

It is also the signal that your window is now too large for the rate you hold. If raising the window produced throttling rather than throughput, the constraint has moved from concurrency to contract, and the next conversation is commercial rather than technical.

A bind that sends nothing looks identical to a bind whose TCP connection has silently died — NAT timeouts and stateful firewalls drop idle sessions without telling either end. enquire_link every 30 to 60 seconds proves the socket is still usable in both directions. Miss the window the operator expects and the SMSC will drop you, usually at the least convenient moment.

Send enquire_link on the same bind you submit on, respond to the SMSC's own enquire_link promptly, and treat a missed response as a dead session: unbind, reconnect, resume. A client that keeps submitting into a half-open socket produces the classic incident where everything looks healthy and nothing is delivered.

How to size a window in practice

  1. Measure the round-trip time on the bind you actually use, not from your laptop. Submit a single message with a window of 1 and time submit_sm to submit_sm_resp.
  2. Divide your contracted TPS by the messages-per-second one round trip allows. That quotient is your minimum viable window.
  3. Add headroom — round up, then add roughly 50% — because round-trip time is not constant and a window sized exactly to the mean will underperform at the median.
  4. Raise it in steps, not in one jump, and watch for ESME_RTHROTTLED. The first sustained throttling tells you where the operator's limit really is.
  5. Confirm the value with the operator. Some SMSCs enforce a maximum window per bind and will silently queue or drop beyond it.
  6. Then, and only then, consider a second bind. Multiple binds multiply concurrency but also multiply the ways a session can fail.

When more binds beat a bigger window

There is a ceiling to what one TCP connection should carry. Beyond a few hundred outstanding PDUs the failure modes get unpleasant: a single socket problem takes all of that traffic with it, head-of-line blocking becomes visible, and recovery after a drop means replaying a large in-flight set. Most operators would rather see several moderate binds than one enormous one, and most will tell you the shape they prefer if asked.

Split by traffic class rather than arbitrarily. Keeping time-critical one-time passcodes on a separate bind from bulk marketing means a marketing burst cannot delay a login code — which is a throughput decision that is really a product decision.

A checklist before you blame the route

  • What is the configured window, and did anyone set it deliberately?
  • Are outstanding sequence numbers timed out and released?
  • Is enquire_link running at the interval the operator expects?
  • Are ESME_RTHROTTLED responses retried with backoff, or counted as failures?
  • Is the measured round-trip time what you assumed when you sized the window?
  • Are receipts (deliver_sm) being acknowledged promptly — an unacknowledged receipt stream can stall the session too?

Frequently asked questions

What is a good SMPP window size?

There is no universal number, because the right value is a function of your round-trip time and your contracted rate. A practical starting point for enterprise traffic is 10, then raise it until you either meet your contracted TPS or start seeing ESME_RTHROTTLED. Sizing it as contracted-TPS × round-trip-time, plus about 50% headroom, gets most integrations close on the first attempt.

Does a bigger window increase my throughput without limit?

No. Once the window is large enough that you reach the operator's rate limit, further increases produce ESME_RTHROTTLED rather than more messages. The window removes a concurrency bottleneck; it cannot raise a commercial ceiling.

Why does my throughput degrade over hours rather than immediately?

This is the classic symptom of outstanding sequence numbers that are never timed out. Every response that goes missing permanently consumes a window slot, so the effective window shrinks over the life of the session until the bind is behaving as if the window were 1. Releasing slots on a timeout fixes it.

Is ESME_RTHROTTLED a permanent failure?

No, and treating it as one loses traffic. It means you exceeded the agreed rate at that instant. Back off, retry the same message, and if throttling is sustained, reduce the window or renegotiate the rate.

Every 30 to 60 seconds is the usual band, and the exact figure should match what the operator expects. The purpose is to prove a possibly idle TCP session is still alive in both directions before a NAT or firewall silently discards it.

Should I use one large bind or several smaller ones?

Several moderate binds are generally healthier than one very large one, because a socket failure then costs you a fraction of your capacity rather than all of it. Split by traffic class — time-critical passcodes separately from bulk — so a marketing burst cannot delay a login code.

Does the window affect delivery receipts as well?

Yes, in the other direction. The SMSC pushes deliver_sm receipts to you, and if your application is slow to return deliver_sm_resp the receipt stream backs up. On a transceiver bind that congestion can affect the whole session, so acknowledge receipts promptly even if you process them asynchronously.

Where can I see this behaviour without connecting to a real SMSC?

Our SMPP session simulator walks a full SMPP 3.4 session in the browser — bind, submit, the asynchronous delivery receipt, enquire_link and unbind — and shows every PDU's fields mapped onto the actual octets on the wire. Nothing connects and nothing is sent.

The short version

If a bind is underperforming, check the window before you check the network. SMPP is asynchronous by design and most integrations run it synchronously by accident. Size the window from measured round-trip time and contracted rate, time out your sequence numbers, keep enquire_link honest, and retry ESME_RTHROTTLED with backoff. That sequence resolves the large majority of throughput complaints without anyone touching a route.

Was this useful?

How we write and review content

Written by

BroadNet Engineering

Engineering operates the wire: SMPP binds, SS7 stacks, routing engines, the boring infrastructure that makes one-billion-message months possible. They write here when something is worth writing down.

The Journal, by email

One essay a month. Nothing else.

Subscribe and we'll send the next piece — engineering, regulation, or industry — the day it ships. No drip campaigns, no product pitches, no third-party sharing. Unsubscribe in one click.

We send one email a month. Unsubscribe anytime. See our privacy policy.

What you'll get
  • Long-form, not link bait
    Six to eleven minutes of focused writing on a single topic.
  • Written by operators
    Engineers, regulatory leads, and product writing in-house.
  • Region-aware
    MENA-grounded view on regulators, operators, and channel mix.
  • Once a month
    No drip campaign. One essay when there is one to share.
Stop reading. Start shipping.

Most of what we write we learned in production.

If this piece made you nod, we should probably talk. Bring the use case, the volumes, the markets — we'll come back with routing, pricing, and a reference customer you can call.

$ curl -X POST https://api.broadnet.me/sms/sendSms