Tikiti at Volume: How We Handle Concurrent STK Pushes at Event Start

S
Samuel Kimani
May 09, 2026 3 min read

The thundering herd is not your server

Ticket sales for a national stadium event open at noon. By 12:00:30 you have tens of thousands of fans hitting Pay. The naive worry is your database; the real bottleneck is Safaricom. Daraja has rate limits per shortcode that aren't publicly documented, and exceeding them returns generic 500s that look like your bug.

Queue, don't fan out

Every STK push request becomes a queued job, not a synchronous Daraja call. The web request returns immediately with a "we're processing your payment" UI; a Horizon worker pool drains the queue at a controlled rate. We tune the worker concurrency to stay below Safaricom's tolerance, empirically around 10-15 STK pushes per second per shortcode, well below the theoretical ceiling.The queue absorbs the spike. Fans see immediate acknowledgment and the STK prompt arrives on their phone within 5-30 seconds depending on queue depth. That's a far better experience than a synchronous flow that 502s at the worst possible moment.

Idempotency on a noisy network

Fans hit "Pay" multiple times when the UI feels slow. Without deduping you queue four payment requests for the same ticket. We key idempotency on (event_id, ticket_type, phone, 30-second window) and refuse duplicates within the window. Subsequent clicks return the existing pending payment instead of queuing a new one.The same idempotency key is checked again when the STK callback arrives. We've seen Safaricom deliver the same successful callback up to four times for the same checkout. The first one wins; the rest are short-circuited with a 200 OK so Safaricom stops retrying.

When Daraja itself misbehaves

At peak, Daraja sometimes accepts an STK request, charges the customer, and never returns a CheckoutRequestID to you. The customer's phone vibrates, they pay, and your system has nothing on file. The polling worker we built for C2B reconciliation handles this too: if the customer paid and we have no checkout record, we match by MpesaReceiptNumber against the ticket queue and issue the ticket retroactively.Another mode: Safaricom returns a CheckoutRequestID but no terminal callback ever arrives, the request hangs in "processing" forever from Daraja's perspective. We poll the STK query endpoint every 60 seconds for any payment older than two minutes and reconcile on whatever Safaricom says is the final state.

What you watch during the spike

Three dashboards: queue depth (how far behind are we?), Daraja error rate (is Safaricom rate-limiting us?), and payment-to-ticket-issued latency (how long until the fan has their ticket?). At peak we target 95th percentile under 30 seconds from STK push to ticket QR delivered. Anything beyond a minute and people start refreshing, which makes everything worse.

Need software built?

Tell us what you need. We respond within 24 hours with a realistic quote.