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. Rate Limiting Your Own Payment Endpoints
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Rate Limiting Your Own Payment Endpoints

Apply rate limiting to every payment-related endpoint. Use express-rate-limit or a Redis-backed limiter. Set strict limits on initialization endpoints (5-10 requests per minute per IP) and generous limits on webhook endpoints (100+ per minute from Paystack IPs). Rate limit by IP, by user, and by business rules like maximum transactions per customer per day.

In this article

  1. 1. Why Rate Limiting Payment Endpoints Matters
  2. 2. Basic Rate Limiting with Express
  3. 3. Redis-Backed Rate Limiting for Production
  4. 4. Rate Limiting Strategies by Endpoint
  5. 5. Business Rule Rate Limiting
  6. 6. Response Best Practices for Rate-Limited Requests
  7. 7. Monitoring Rate Limit Events

Why Rate Limiting Payment Endpoints Matters

Paystack has its own API rate limits to protect their infrastructure. But your payment endpoints need their own rate limiting for a different reason: to protect your business from abuse.

Without rate limiting, attackers can:

  • Card testing. Test hundreds of stolen card numbers per minute against your checkout to find which cards are valid. Each test is a small transaction (100 kobo) that gets reversed, but you pay processing fees for each one.
  • Brute force verification. Guess transaction references and call your verification endpoint repeatedly, looking for a successful transaction they can claim.
  • Denial of service. Flood your webhook endpoint with fake payloads, causing your server to spend all its resources on signature verification.
  • Transaction reference enumeration. Try sequential references to discover valid transactions and their details.

Rate limiting does not replace other security measures (signature verification, amount validation, idempotency). It is a layer of defense that reduces the speed and scale of attacks.

Basic Rate Limiting with Express

The simplest approach uses the express-rate-limit package:

var rateLimit = require('express-rate-limit');

// Strict limit for payment initialization
var paymentInitLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 5, // 5 requests per minute per IP
  standardHeaders: true,
  legacyHeaders: false,
  message: {
    status: 429,
    error: 'Too many payment attempts. Please wait a minute and try again.',
  },
});

// Moderate limit for verification
var verifyLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
  message: {
    status: 429,
    error: 'Too many verification requests. Please wait.',
  },
});

// Generous limit for webhooks (Paystack retries)
var webhookLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
});

app.post('/api/payment/initialize', paymentInitLimiter, handleInitialize);
app.get('/api/payment/verify', verifyLimiter, handleVerify);
app.post('/webhook/paystack', webhookLimiter, handleWebhook);

This in-memory approach works for a single server process. For production with multiple server instances, you need a shared store like Redis.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Redis-Backed Rate Limiting for Production

In-memory rate limiters have two problems: they reset when the server restarts, and they do not share state across multiple server instances. A Redis-backed limiter solves both.

var rateLimit = require('express-rate-limit');
var RedisStore = require('rate-limit-redis');
var Redis = require('ioredis');

var redis = new Redis(process.env.REDIS_URL);

var paymentInitLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: function() {
      return redis.call.apply(redis, arguments);
    },
  }),
  windowMs: 60 * 1000,
  max: 5,
  standardHeaders: true,
  keyGenerator: function(req) {
    // Rate limit by IP
    return 'ratelimit:init:' + req.ip;
  },
});

var userPaymentLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: function() {
      return redis.call.apply(redis, arguments);
    },
  }),
  windowMs: 24 * 60 * 60 * 1000, // 24 hours
  max: 20, // 20 transactions per day per user
  standardHeaders: true,
  keyGenerator: function(req) {
    // Rate limit by authenticated user
    return 'ratelimit:user:' + (req.user ? req.user.id : req.ip);
  },
});

app.post('/api/payment/initialize', paymentInitLimiter, userPaymentLimiter, handleInitialize);

Notice the two limiters stacked on the initialization endpoint: one limits by IP (5 per minute) and one limits by user (20 per day). An attacker using a single IP cannot exceed 5 attempts per minute. An authenticated user cannot exceed 20 transactions per day, regardless of how many IPs they use.

Rate Limiting Strategies by Endpoint

Payment Initialization

This is the endpoint attackers target most for card testing. Apply the strictest limits:

  • 5-10 requests per minute per IP
  • 20 requests per day per authenticated user
  • Require authentication before allowing initialization

Transaction Verification

Moderate limits. Legitimate users may verify the same transaction multiple times (page refreshes, retries):

  • 20 requests per minute per IP
  • Only accept references that match orders in your database (reject random reference guesses)

Webhook Endpoint

Be generous here. Paystack sends legitimate retries, and during busy periods you may receive many webhooks in quick succession:

  • 100-200 requests per minute total (not per IP, since all come from Paystack)
  • Combine with IP allowlisting for Paystack's webhook IPs
  • Signature verification is your primary defense here, not rate limiting

Refund and Transfer Endpoints (Admin)

Very strict limits. These endpoints move money:

  • 3-5 requests per minute per admin user
  • Require multi-factor authentication for each action
  • Log every request with the admin user, IP, and action details

Business Rule Rate Limiting

Beyond simple request-per-minute limits, implement business-rule-based limiting that catches attacks that stay under the per-minute threshold:

async function checkBusinessRules(req) {
  var email = req.body.email;
  var ip = req.ip;

  // Rule 1: Max 3 failed payments per email per hour
  var recentFailures = await db.query(
    'SELECT COUNT(*) FROM payment_attempts WHERE email = $1 AND status = $2 AND created_at > NOW() - INTERVAL '1 hour'',
    [email, 'failed']
  );

  if (parseInt(recentFailures.rows[0].count) >= 3) {
    return { blocked: true, reason: 'Too many failed payment attempts' };
  }

  // Rule 2: Max 5 different emails from the same IP per hour
  var ipEmails = await db.query(
    'SELECT COUNT(DISTINCT email) FROM payment_attempts WHERE ip_address = $1 AND created_at > NOW() - INTERVAL '1 hour'',
    [ip]
  );

  if (parseInt(ipEmails.rows[0].count) >= 5) {
    return { blocked: true, reason: 'Too many different emails from this IP' };
  }

  // Rule 3: Max transaction amount per day per user
  var dailyTotal = await db.query(
    'SELECT COALESCE(SUM(amount), 0) as total FROM successful_payments WHERE user_id = $1 AND created_at > NOW() - INTERVAL '24 hours'',
    [req.user.id]
  );

  if (parseInt(dailyTotal.rows[0].total) > 10000000) { // 100,000 NGN daily limit
    return { blocked: true, reason: 'Daily transaction limit reached' };
  }

  return { blocked: false };
}

app.post('/api/payment/initialize', paymentInitLimiter, async function(req, res) {
  var ruleCheck = await checkBusinessRules(req);
  if (ruleCheck.blocked) {
    return res.status(429).json({ error: ruleCheck.reason });
  }

  // Record the attempt
  await db.query(
    'INSERT INTO payment_attempts (email, ip_address, amount, status) VALUES ($1, $2, $3, $4)',
    [req.body.email, req.ip, req.body.amount, 'attempted']
  );

  // Proceed with initialization
  await handleInitialize(req, res);
});

Business rules catch sophisticated attacks that rotate IPs or use botnets to stay under per-IP rate limits. The rules above catch card testing (many failures), distributed attacks (many emails from one IP), and high-value fraud (daily limits).

Response Best Practices for Rate-Limited Requests

How you respond to rate-limited requests matters for both security and user experience:

  • Return 429 (Too Many Requests). This is the correct HTTP status code. Do not return 200 (confuses the client), 500 (implies a server error), or 403 (implies authentication issues).
  • Include a Retry-After header. Tell the client how many seconds to wait before trying again.
  • Return a human-readable message. "Please wait a minute and try again" is better than a technical error code.
  • Do not reveal the limit thresholds. Saying "You have exceeded the limit of 5 requests per minute" tells attackers exactly how fast they can go. Just say "Too many requests."
var rateLimitHandler = function(req, res) {
  res.set('Retry-After', '60');
  res.status(429).json({
    status: false,
    message: 'Too many requests. Please wait and try again.',
  });
};

Monitoring Rate Limit Events

Every rate limit hit should be logged and monitored. A sudden increase in rate-limited requests means an attack is in progress.

var paymentInitLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  handler: function(req, res) {
    console.log(
      'Rate limited: ' + req.ip +
      ' endpoint=' + req.path +
      ' user=' + (req.user ? req.user.id : 'anonymous')
    );

    // Increment a counter for alerting
    incrementMetric('rate_limit_hits', {
      endpoint: req.path,
      ip: req.ip,
    });

    res.status(429).json({
      status: false,
      message: 'Too many requests. Please wait and try again.',
    });
  },
});

Set up alerts for thresholds like:

  • More than 50 rate limit hits in 10 minutes (attack in progress)
  • Rate limit hits from more than 10 unique IPs on the same endpoint in 5 minutes (distributed attack)
  • Rate limit hits on the verification endpoint with nonexistent references (reference enumeration)

Key Takeaways

  • ✓Rate limiting your payment endpoints is your first line of defense against card testing attacks, where attackers rapidly test stolen card numbers against your checkout.
  • ✓Apply different rate limits to different endpoints: strict limits for initialization (5-10 per minute per IP), moderate for verification (20 per minute per IP), and generous for webhooks (100+ per minute).
  • ✓Rate limit by multiple dimensions: IP address, authenticated user, and business rules like maximum transactions per customer per day.
  • ✓Use Redis-backed rate limiters for production. In-memory limiters reset on server restart and do not work across multiple server instances.
  • ✓Return 429 (Too Many Requests) with a Retry-After header. Do not return 500 or 200 for rate-limited requests.
  • ✓Log rate limit hits for security monitoring. A sudden spike in rate-limited requests indicates an attack in progress.

Frequently Asked Questions

Does Paystack have its own rate limits?
Yes, Paystack rate limits API requests to protect their infrastructure. But their limits are designed for their infrastructure capacity, not for your business logic. You need your own limits to protect against card testing, abuse, and denial of service at your endpoint level.
Will rate limiting affect legitimate customers?
Not if you set reasonable limits. A legitimate customer rarely makes more than 2-3 payment attempts per minute. A limit of 5 per minute gives plenty of room for retries without allowing abuse. Monitor false positives and adjust thresholds based on your traffic patterns.
Should I rate limit by IP behind a reverse proxy?
Yes, but make sure you trust the correct header. Behind Nginx, Cloudflare, or a load balancer, the client IP is in X-Forwarded-For or X-Real-IP. Express trusts these headers only if you set app.set("trust proxy", true). Without this, all requests appear to come from the proxy IP.
Can attackers bypass IP-based rate limiting?
Yes, by rotating IPs through VPNs, proxies, or botnets. That is why you should combine IP-based limits with user-based limits and business rule limits. The more dimensions you rate limit on, the harder it is for an attacker to circumvent them.
Should I block or slow down rate-limited requests?
Return 429 immediately for most use cases. Deliberate slowing (adding a delay before responding) can tie up server resources. For webhook endpoints, return 429 quickly so Paystack knows to retry later without consuming your server resources.

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

Paystack Security, Keys and PCI Compliance: Complete Guide

A complete security reference for Paystack integrations. Covers API key types and rotation, secure key storage, PCI DSS compliance scope, price tampering prevention, webhook replay attacks, rate limiting, fraud signals, and logging without exposing secrets.

Read more

Never Commit Your Paystack Key: Detection and Remediation

Committing a Paystack secret key to version control is one of the most common and most dangerous mistakes in payment integrations. This guide shows you how to detect committed keys, set up automated prevention with pre-commit hooks, and remediate the damage when a key does slip through.

Read more

Price Tampering Attacks on Paystack Checkouts

Price tampering is the most common attack on Paystack integrations. The attacker modifies the amount in the frontend checkout initialization, paying a fraction of the actual price. This guide shows how the attack works and how server-side amount validation stops it completely.

Read more

Logging Payments Without Logging Secrets

Good payment logging helps you debug failed charges and reconcile transactions. Bad payment logging leaks your Paystack secret key, card tokens, or customer data into log files that attackers can read. This guide draws a clear line between safe and dangerous payment log data.

Read more

Debugging Paystack with the Paystack CLI

The Paystack CLI gives you terminal access to your Paystack account. Instead of clicking through the Dashboard to check a transaction or waiting for a webhook to arrive, you run a command and get the answer in seconds. For developers who live in the terminal, it is the fastest way to debug payment issues.

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.