McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Handling Slow Networks in Paystack Checkout Flows
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Handling Slow Networks in Paystack Checkout Flows

To handle slow networks in Paystack checkout flows, set generous timeouts on your API calls (at least 30 seconds for initialize, longer for verify), show meaningful loading states that tell the customer something is happening, implement retry logic with exponential backoff for server-to-Paystack calls, and never assume a timeout means failure because the payment may still complete.

In this article

  1. 1. The Network Reality in African Markets
  2. 2. Timeout Configuration
  3. 3. Loading States That Prevent Abandonment
  4. 4. Retry Logic for Server-Side Calls
  5. 5. Preventing Double-Clicks and Double-Submits
  6. 6. A Timeout Does Not Mean Failure
  7. 7. Optimizing Payload Size
  8. 8. Build for Real Networks

The Network Reality in African Markets

If you are building payment flows for customers in Nigeria, Kenya, Ghana, or South Africa, you are building for network conditions that most payment tutorials do not account for.

A typical scenario: a customer on Safaricom 3G in Nairobi clicks "Pay." The request from their phone to your server takes 2 seconds. Your server calls Paystack's API, which takes another 1.5 seconds. Paystack talks to the bank, which takes 3 seconds. The response travels back through the same chain. Total time: 8-12 seconds. During those 12 seconds, the customer sees nothing unless you planned for it.

Things that happen regularly on African mobile networks:

  • Latency spikes from 200ms to 3000ms mid-request
  • Complete connection drops lasting 5-30 seconds when moving between cell towers
  • Throttled connections during peak hours (evenings, end of month)
  • DNS resolution failures that look like server errors
  • TCP connections that succeed but then stall during data transfer

Your checkout flow needs to handle all of these gracefully. Not with retry-until-crash logic, but with deliberate timeout configuration, meaningful user feedback, and safety checks that prevent duplicate charges.

Timeout Configuration

Set your timeouts based on the slowest networks your customers use, not the fastest ones you develop on.

Server-side timeouts (your server calling Paystack)

// Initialize transaction with a generous timeout
var controller = new AbortController();
var timeoutId = setTimeout(function() { controller.abort(); }, 30000); // 30 seconds

try {
  var response = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: email,
      amount: amount,
      reference: reference,
    }),
    signal: controller.signal,
  });

  clearTimeout(timeoutId);
  var data = await response.json();
  // Process response
} catch (err) {
  clearTimeout(timeoutId);
  if (err.name === 'AbortError') {
    // Timeout: the request may or may not have reached Paystack
    // Do NOT assume it failed. Check if the reference exists.
    console.error('Initialize timed out for reference: ' + reference);
  } else {
    // Network error
    console.error('Network error:', err.message);
  }
}

Why 30 seconds? The Paystack API itself responds quickly (usually under 2 seconds), but the network path between your server and Paystack can add significant latency. If your server is in Europe and Paystack is in Lagos, the round trip already takes 200-400ms. Add network congestion and you can easily hit 5-10 seconds for a single request. A 30-second timeout gives you headroom for the worst cases without waiting forever.

Client-side timeouts (customer's browser to your server)

The customer's device to your server is the slowest link in the chain. Set a generous client-side timeout and show a loading state:

async function initializePayment(email, amount) {
  var controller = new AbortController();
  var timeoutId = setTimeout(function() { controller.abort(); }, 45000); // 45 seconds

  try {
    var response = await fetch('/api/payment/initialize', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: email, amount: amount }),
      signal: controller.signal,
    });

    clearTimeout(timeoutId);
    return await response.json();
  } catch (err) {
    clearTimeout(timeoutId);
    if (err.name === 'AbortError') {
      return { error: 'The connection is slow. Please check your network and try again.' };
    }
    return { error: 'Could not connect. Please check your internet connection.' };
  }
}

Verify endpoint timeout: Set verify timeouts slightly longer than initialize timeouts. If the verify call times out, you must not tell the customer their payment failed. The payment may have succeeded. Check the Transaction Timeline API or wait for the webhook.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Loading States That Prevent Abandonment

A spinner with no context is anxiety-inducing when you are about to spend money. After 5 seconds of a generic spinner, most customers assume something went wrong. After 10 seconds, they close the tab.

Progressive status messages:

Instead of a single spinner, show messages that progress as time passes. This tells the customer that things are happening, even if the network is slow:

function showProgressiveLoading(containerId) {
  var messages = [
    { delay: 0, text: 'Connecting to payment gateway...' },
    { delay: 3000, text: 'Setting up your checkout...' },
    { delay: 8000, text: 'Almost ready. Slow network detected...' },
    { delay: 15000, text: 'Still working. Please do not close this page...' },
    { delay: 25000, text: 'This is taking longer than usual. Please wait...' },
  ];

  var container = document.getElementById(containerId);
  var timeouts = [];

  for (var i = 0; i < messages.length; i++) {
    (function(msg) {
      var tid = setTimeout(function() {
        container.textContent = msg.text;
      }, msg.delay);
      timeouts.push(tid);
    })(messages[i]);
  }

  // Return a cleanup function
  return function cleanup() {
    for (var j = 0; j < timeouts.length; j++) {
      clearTimeout(timeouts[j]);
    }
  };
}

// Usage
var cleanup = showProgressiveLoading('loading-message');
var result = await initializePayment(email, amount);
cleanup();

The "please do not close this page" message is important. On slow networks, the natural instinct is to close and try again. But if the initialization request is in flight, closing the page and trying again can create a duplicate transaction (if you generate a new reference) or the customer returns to a stale state.

What not to show:

  • Technical details ("HTTP request in progress" or "Waiting for API response"). Customers do not know what an API is and should not need to.
  • Percentage progress bars. You do not actually know what percentage of the process is done. A progress bar stuck at 45% for 20 seconds is worse than no progress bar at all.
  • Nothing at all. A blank page or a frozen screen is the worst option. Always show something.

Retry Logic for Server-Side Calls

When your server calls Paystack and gets a network error (not an API error), retrying makes sense. But retrying carelessly creates duplicate transactions, so the rules are strict.

Safe to retry:

  • Initialize Transaction calls (Paystack uses the reference for idempotency, so the same reference produces the same transaction)
  • Verify Transaction calls (read-only, no side effects)
  • List/fetch calls (read-only)

Dangerous to retry:

  • Charge API calls without a reference (could create duplicate charges)
  • Refund calls (could issue duplicate refunds)
  • Any write operation without idempotency
async function fetchWithRetry(url, options, maxRetries) {
  var attempt = 0;
  var lastError = null;

  while (attempt < maxRetries) {
    try {
      var controller = new AbortController();
      var timeoutId = setTimeout(function() { controller.abort(); }, 30000);

      var fetchOptions = Object.assign({}, options, { signal: controller.signal });
      var response = await fetch(url, fetchOptions);
      clearTimeout(timeoutId);

      // If Paystack returned a response (even an error), do not retry
      // API errors (400, 401, etc.) are not network issues
      return response;
    } catch (err) {
      clearTimeout(timeoutId);
      lastError = err;
      attempt++;

      if (attempt < maxRetries) {
        // Exponential backoff: 1s, 2s, 4s
        var delay = Math.pow(2, attempt - 1) * 1000;
        await new Promise(function(resolve) { setTimeout(resolve, delay); });
      }
    }
  }

  throw lastError;
}

// Usage: retry initialize up to 3 times
var response = await fetchWithRetry(
  'https://api.paystack.co/transaction/initialize',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email: email, amount: amount, reference: reference }),
  },
  3
);

Notice the retry only fires on network errors (the catch block). If Paystack returns a 400 or 500 response, that is an API response and retrying will not help. Handle it as a business error.

Preventing Double-Clicks and Double-Submits

On a slow connection, the customer clicks "Pay," nothing visible happens for 3 seconds, and they click again. If each click fires a new request with a new reference, you might initialize two separate transactions. If they complete both (unlikely but possible), you have a duplicate charge.

Client-side: disable the button

var payButton = document.getElementById('pay-button');

payButton.addEventListener('click', async function() {
  // Disable immediately
  payButton.disabled = true;
  payButton.textContent = 'Processing...';

  try {
    var result = await initializePayment(email, amount);

    if (result.authorization_url) {
      window.location.href = result.authorization_url;
    } else {
      // Re-enable on error so they can try again
      payButton.disabled = false;
      payButton.textContent = 'Pay ' + formatCurrency(amount, currency);
      showError(result.error);
    }
  } catch (err) {
    payButton.disabled = false;
    payButton.textContent = 'Pay ' + formatCurrency(amount, currency);
    showError('Something went wrong. Please try again.');
  }
});

Server-side: use deterministic references

Even if the client double-submits, a deterministic reference means both requests create the same Paystack transaction:

// Same order always gets the same reference
var reference = 'order_' + orderId;

// If the client sends this twice, Paystack returns the same transaction both times

The combination of client-side button disabling and server-side deterministic references gives you two layers of protection. The button prevents most double-clicks. The reference prevents the rest.

A Timeout Does Not Mean Failure

This is the most important concept in this entire guide. When a network request to Paystack times out, you do not know what happened. Three scenarios are all possible:

  1. The request never reached Paystack. No transaction exists. Safe to retry.
  2. The request reached Paystack, but the response was lost. The transaction was created. Retrying with the same reference is safe (Paystack returns the existing transaction). Retrying with a new reference creates a duplicate.
  3. The request reached Paystack and the charge is in progress. The customer's bank is processing the charge. The payment might succeed in a few seconds.

What to do after a timeout:

async function handleInitializeTimeout(reference) {
  // Wait a moment, then check if the transaction exists
  await new Promise(function(resolve) { setTimeout(resolve, 3000); });

  try {
    var response = await fetch(
      'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
      {
        headers: {
          Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        },
      }
    );

    var data = await response.json();

    if (data.status && data.data) {
      // Transaction exists on Paystack
      if (data.data.status === 'success') {
        return { status: 'paid', data: data.data };
      }
      if (data.data.status === 'abandoned') {
        return { status: 'retry', message: 'Previous attempt was not completed. Please try again.' };
      }
      // Transaction exists but is pending or failed
      return { status: 'pending', data: data.data };
    }

    // Transaction does not exist on Paystack. Safe to retry.
    return { status: 'retry', message: 'Connection issue. Please try again.' };
  } catch (err) {
    // Even the verify call timed out. Network is very bad.
    return { status: 'unknown', message: 'Network issue. Please check your connection and try again.' };
  }
}

Never show "Payment failed" after a timeout. Show "We could not confirm your payment. Checking..." and then verify. If you tell the customer it failed when it actually succeeded, they will either pay again (double charge) or leave angry (lost sale).

Optimizing Payload Size

On a 150kbps connection, every kilobyte matters. A 50KB JavaScript bundle takes 2.7 seconds to download. A 200KB bundle takes 10.7 seconds. Optimize what you send to the client.

Checkout page weight targets:

  • HTML: under 10KB
  • CSS: under 15KB (only checkout-specific styles, not your entire site CSS)
  • JavaScript: under 30KB (excluding Paystack's inline script, which you cannot control)
  • Images: zero. The checkout page does not need product images. Show the product name and amount as text.

API response size:

Keep your API responses lean. Your /api/payment/initialize endpoint should return only what the frontend needs (authorization URL, access code, reference), not the entire Paystack response object.

// Lean response from your server
res.json({
  url: data.data.authorization_url,
  code: data.data.access_code,
  ref: data.data.reference,
});
// ~120 bytes instead of the full Paystack response (~2KB)

These optimizations sound small, but they compound. A checkout page that loads in 3 seconds instead of 8 seconds on a slow connection can make a meaningful difference in how many customers complete payment.

Build for Real Networks

Most payment integration tutorials assume fast, reliable internet. Building for African markets means building for the networks your customers actually use. For the complete picture of handling payments on unreliable connections, including fully offline scenarios, see offline-tolerant payment flows.

For the foundation of everything in this guide, start with accepting payments with Paystack: the complete guide.

Create a free McTaba account

Key Takeaways

  • ✓A timeout on your initialize or verify call does not mean the payment failed. The request may have reached Paystack successfully. Always check the transaction status before showing an error.
  • ✓Set API call timeouts to at least 30 seconds for Paystack endpoints. African network latency can spike to several seconds even on 4G, and the Paystack API itself needs time to process.
  • ✓Show progressive loading states, not spinners. "Connecting to payment gateway..." then "Waiting for bank response..." tells the customer something is happening. A generic spinner for 15 seconds feels like something is broken.
  • ✓Implement exponential backoff for retries on server-side Paystack API calls. Retry immediately on network errors, but wait longer between each attempt.
  • ✓Disable the pay button after the first click and do not re-enable it until the flow completes or fails conclusively. Double-clicks on slow connections cause duplicate transactions.
  • ✓Design your checkout to work on 150kbps connections. If it only works on broadband, you are excluding a large portion of African mobile users.

Frequently Asked Questions

What timeout should I set for Paystack API calls?
Set at least 30 seconds for initialize and verify calls from your server. For client-side requests (customer browser to your server), set 45 seconds. These seem long, but African mobile networks can have multi-second latency spikes. A timeout that is too short causes failures on slow but functional connections.
Should I retry Paystack API calls when they timeout?
Retry only safe operations. Initialize Transaction is safe to retry because Paystack uses the reference for idempotency. Verify is safe because it is read-only. Do not retry charge or refund calls without verifying the state first, as you could create duplicate charges or refunds.
How do I prevent duplicate charges on slow networks?
Use two layers. On the client, disable the pay button after the first click and do not re-enable it until you get a definitive result. On the server, use deterministic transaction references tied to the order ID so that duplicate requests create the same Paystack transaction, not separate ones.
What should I show the customer while the payment is processing on a slow network?
Show progressive text messages that change over time: "Connecting to payment gateway" at 0 seconds, "Setting up your checkout" at 3 seconds, "Slow network detected, please wait" at 8 seconds, "Still working, please do not close this page" at 15 seconds. Avoid generic spinners with no context.
How do I test my checkout on slow networks?
Use Chrome DevTools network throttling set to "Slow 3G" for quick tests, but also test on a real Android phone with a real SIM card. Emulated throttling does not fully replicate real network behavior like connection drops, DNS failures, and TCP stalls. Buy a mid-range phone that matches what your customers use.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Accepting Payments with Paystack: Complete Guide

Everything you need to accept your first payment with Paystack: choosing between Initialize Transaction and the Charge API, handling amounts in kobo, attaching metadata, setting callback URLs, and designing transaction references that keep your system clean.

Read more

Paystack Checkout Conversion: Engineering Decisions That Move the Number

Technical decisions that directly affect whether customers complete Paystack checkout. Covers channel ordering, page load speed, error message recovery, mobile optimization, and trust signal placement.

Read more

Offline-Tolerant Payment Flows with Paystack

In many African markets, network connectivity is unreliable. Customers start payments on slow 2G connections, lose signal mid-checkout, or switch between WiFi and mobile data. This guide shows you how to build payment flows that work even when the network does not.

Read more

Handling Abandoned Paystack Checkouts

A customer clicks "Pay," the Paystack checkout opens, and then they close the tab. The transaction sits as "abandoned" in your dashboard. This guide covers how to detect abandoned checkouts, recover lost sales with follow-up emails, and reduce abandonment through better checkout design.

Read more

Testing Paystack Webhooks in CI Without a Public URL

Your CI server has no public URL. Paystack cannot reach it. This guide shows how to test your webhook handler in GitHub Actions, GitLab CI, and other pipelines by generating valid HMAC-signed payloads locally and sending them to your handler with HTTP requests.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.