What is horizontal scaling?
Horizontal scaling, or scaling out, means handling more work by running more copies of a service side by side: more API instances behind a load balancer, more workers pulling from a queue. Vertical scaling, or scaling up, means moving the same single instance to a bigger machine with more CPU and memory. Scaling out has no hard ceiling in principle, adds redundancy (the basis of high availability) as a side effect and can follow demand up and down automatically; scaling up is simpler but tops out at the largest machine and leaves a single point of failure.
Scaling out works when instances are interchangeable, which means stateless. On a WhatsApp backend the stateless parts (the webhook receiver, the send API, the workers that call the Cloud API) scale out easily. The stateful parts need work first: conversation and session state must live in a shared store rather than in memory, scheduled broadcasts must run on exactly one instance, per-conversation order must be kept by partitioning work by the customer's wa_id, and every new instance opens more database connections.
WhatsApp adds a ceiling that no number of servers can lift: throughput (MPS) is set per business phone number, up to 80 messages per second by default and 1,000 after Meta's automatic upgrade for eligible numbers. Workers beyond that rate just collect Error 130429 (Rate Limit Hit). Scaling a WhatsApp sender therefore means scaling workers up to the cap, sharing a rate limiter across them, and adding phone numbers or qualifying for the upgrade when one number's cap is the bottleneck.
Horizontal scaling is often mentioned alongside load balancing, which spreads incoming requests across instances, and a message queue, which lets workers share a backlog. Scaling decides how much capacity exists; balancing and queueing decide how work reaches it. Load testing is how you find out where scaling out stops helping.
Key takeaways
- Horizontal scaling adds more instances to share the load; vertical scaling makes one instance bigger.
- Stateless API servers and queue workers scale out easily; sessions, locks, schedulers and ordering need shared state first.
- More workers cannot beat a number's throughput: up to 80 messages per second by default, 1,000 after Meta's upgrade, with error 130429 beyond it.
- Autoscale messaging workers on queue depth or age, not CPU, and cap the maximum at what downstream limits allow.
- Horizontal scaling adds capacity; load balancing spreads traffic across it.
Why the difference matters
Why does horizontal scaling matter?
Message traffic is spiky: a Diwali campaign, a payday reminder run or a viral Click-to-WhatsApp ad can multiply volume tenfold in minutes. A backend that only scales up either sits on an oversized machine most of the year or falls over on the busiest day. Scaling out lets capacity follow demand, survives the loss of any one instance, and keeps costs closer to actual use. Knowing its limits, above all the per-number throughput cap, stops teams from paying for servers that cannot make a broadcast any faster.
How does horizontal scaling work?
- Step 1
Make instances stateless
Move sessions, conversation state, idempotency records and caches to a shared store such as Redis or the database, so any instance can handle any request.
- Step 2
Put a queue between intake and sending
The API and webhook receiver enqueue work and return quickly; a pool of workers drains the queue. Each side then scales on its own signal.
- Step 3
Partition to keep order
Route all jobs for one conversation to the same partition or ordered group by the customer's
wa_id, so adding workers does not shuffle a customer's messages. - Step 4
Share limits across instances
Enforce each phone number's messages-per-second budget with a shared limiter (the token bucket algorithm in Redis, for example), not a per-process one that multiplies with every new worker.
- Step 5
Autoscale on the right signal
Scale workers on consumer lag (queue depth or the age of the oldest job), keep a minimum of two for redundancy, and cap the maximum at what the throughput limit and database connections allow.
Example: A Bengaluru D2C brand's festive send
A Bengaluru skincare brand plans 300,000 marketing templates across two phone numbers, each at the default 80 messages per second, so 160 per second is the most the sends can go. Its first run autoscaled workers on CPU to 40 replicas; most of them spent the campaign receiving 130429. The second run scaled on queue depth with a maximum of 12 workers, a shared token bucket per phone_number_id set just under each number's limit, and a database connection pooler in front of Postgres. Throughput held at about 155 per second and the send finished in roughly 32 minutes without a throttling spike.
Horizontal scaling in code
apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: { name: wa-send-worker }spec:scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: wa-send-worker }minReplicas: 2 # redundancy even when idlemaxReplicas: 12 # beyond this, the per-number throughput cap is the limitmetrics:- type: External # needs an external-metrics adapter for your queueexternal:metric:name: queue_messages_readyselector: { matchLabels: { queue: wa-send } }target:type: AverageValueaverageValue: "500" # aim for ~500 waiting jobs per worker
Scale workers on the backlog, with a floor for redundancy and a ceiling set by downstream limits.
Horizontal scaling: key facts and limits
- Horizontal (scale out)
- More instances; needs stateless, interchangeable services
- Vertical (scale up)
- A bigger machine; simple, but capped and a single point of failure
- Per-number throughput
- Up to 80 mps by default, 1,000 after Meta's upgrade
- Over the throughput cap
- Error 130429
- Coexistence numbers
- 20 mps, incoming and outgoing combined
- Sizing rule (Little's law)
- Busy workers = arrivals per second x seconds per job
Common mix-ups
Common mistakes with horizontal scaling
Scaling past the downstream cap
Doubling workers does not double sends once a number is at its throughput limit; it only produces
130429errors and retries. Cap autoscaling at the limit and add numbers or qualify for the upgrade instead.Hidden state in memory
In-memory sessions, local caches of conversation state and cron jobs inside the app break or run twice when a second instance appears. Externalise state and run schedulers with a lock or leader election.
Running out of database connections
Twenty instances with a pool of 20 each is 400 connections, more than many databases allow. Use a connection pooler and size pools from the database's limit downwards.
Autoscaling on CPU
Send workers spend most of their time waiting on the network, so CPU stays low while the queue grows. Scale on queue depth or oldest-job age instead.
Breaking per-conversation order
With many workers, two messages to the same customer can be sent out of order. Partition by
wa_idor use ordered message groups so each conversation is handled in sequence.
Rules of thumb
Best practices for horizontal scaling
- Load test with a mock of the Cloud API to find your real ceiling before a big campaign.
- Scale the webhook receiver separately from send workers; inbound bursts and outbound campaigns peak at different times.
- Keep at least two instances of everything, even at night: scaling out is also your redundancy.
- Watch time-to-drain (queue depth divided by send rate) during campaigns, not only replica counts.