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 Limits and Caching for Identity Lookups
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Rate Limits and Caching for Identity Lookups

Cache Resolve Account Number results in your database for 30 days. Cache Resolve Card BIN results indefinitely since BIN data never changes. Use a request queue to throttle outgoing calls to Paystack. Implement a cache-first lookup pattern: check your database before calling the API. For high-traffic applications, add a Redis cache layer in front of the database.

In this article

  1. 1. Understanding Paystack Rate Limits
  2. 2. Database Cache Schema
  3. 3. Implementing Cache-First Lookups
  4. 4. Adding a Redis Cache Layer
  5. 5. Throttling Outgoing Requests
  6. 6. Cache Invalidation
  7. 7. Handling 429 Responses

Understanding Paystack Rate Limits

Paystack applies rate limits to all API endpoints, including identity lookups. When you exceed the limit, the API returns a 429 status code with a message telling you to slow down.

Why rate limits exist: Identity endpoints query upstream bank systems. Every Resolve Account Number call goes from Paystack to the destination bank and back. If thousands of merchants hit this endpoint simultaneously without limits, the upstream bank systems would be overwhelmed.

How limits work: Paystack tracks your API calls per time window. The exact numbers depend on your account type and agreement. When you exceed the allowed calls in a window, subsequent calls return 429 until the window resets.

What triggers rate limits in practice:

  • Resolving the same account number repeatedly (user refreshes the page, form resubmits).
  • Batch operations that resolve hundreds of accounts in a loop without delays.
  • Bugs that cause infinite retry loops on failed lookups.
  • Frontend code that calls your resolve endpoint on every keystroke instead of waiting for complete input.

Caching eliminates most of these problems. If you cache properly, you rarely hit rate limits because most lookups resolve from your local storage.

Database Cache Schema

Store resolved results in your database. This is your primary cache layer.

-- Identity lookup cache tables
CREATE TABLE account_resolution_cache (
  id SERIAL PRIMARY KEY,
  account_number TEXT NOT NULL,
  bank_code TEXT NOT NULL,
  account_name TEXT NOT NULL,
  resolved_at TIMESTAMP NOT NULL DEFAULT NOW(),
  expires_at TIMESTAMP NOT NULL DEFAULT (NOW() + INTERVAL '30 days'),
  UNIQUE(account_number, bank_code)
);

CREATE INDEX idx_resolution_lookup
  ON account_resolution_cache(account_number, bank_code)
  WHERE expires_at > NOW();

CREATE TABLE bin_cache (
  bin CHAR(6) PRIMARY KEY,
  brand TEXT NOT NULL,
  bank TEXT,
  card_type TEXT,
  country_code CHAR(2),
  country_name TEXT,
  cached_at TIMESTAMP NOT NULL DEFAULT NOW()
  -- No expiry: BIN data is permanent
);

Account resolution cache expires in 30 days. Account holder names change when people marry, legally change their name, or close and reopen accounts. 30 days is a reasonable window that balances API call savings against data freshness.

BIN cache never expires. The first 6 digits of a card number are permanently assigned to a specific bank and card network. Once cached, the data is valid forever.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Implementing Cache-First Lookups

// cached-identity.js
const axios = require('axios');

async function resolveAccountCached(accountNumber, bankCode) {
  // Step 1: Check database cache
  var cached = await db.query(
    'SELECT account_name, resolved_at FROM account_resolution_cache ' +
    'WHERE account_number = $1 AND bank_code = $2 AND expires_at > NOW()',
    [accountNumber, bankCode]
  );

  if (cached.rows.length > 0) {
    return {
      account_name: cached.rows[0].account_name,
      from_cache: true,
      cached_at: cached.rows[0].resolved_at,
    };
  }

  // Step 2: Call Paystack
  var response = await axios.get(
    'https://api.paystack.co/bank/resolve',
    {
      params: { account_number: accountNumber, bank_code: bankCode },
      headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY },
      timeout: 15000,
    }
  );

  var accountName = response.data.data.account_name;

  // Step 3: Store in cache
  await db.query(
    'INSERT INTO account_resolution_cache (account_number, bank_code, account_name) ' +
    'VALUES ($1, $2, $3) ' +
    'ON CONFLICT (account_number, bank_code) ' +
    'DO UPDATE SET account_name = $3, resolved_at = NOW(), expires_at = NOW() + INTERVAL '30 days'',
    [accountNumber, bankCode, accountName]
  );

  return {
    account_name: accountName,
    from_cache: false,
  };
}

async function resolveBinCached(bin) {
  // Check cache first
  var cached = await db.query(
    'SELECT brand, bank, card_type, country_code, country_name FROM bin_cache WHERE bin = $1',
    [bin]
  );

  if (cached.rows.length > 0) {
    return cached.rows[0];
  }

  // Call Paystack
  var response = await axios.get(
    'https://api.paystack.co/decision/bin/' + bin,
    { headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY } }
  );

  var data = response.data.data;

  // Cache permanently
  await db.query(
    'INSERT INTO bin_cache (bin, brand, bank, card_type, country_code, country_name) ' +
    'VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (bin) DO NOTHING',
    [bin, data.brand, data.bank, data.card_type, data.country_code, data.country_name]
  );

  return data;
}

Adding a Redis Cache Layer

For high-traffic applications, add Redis in front of the database. Redis is faster for lookups (microseconds vs milliseconds) and reduces database load.

// redis-cache.js
var Redis = require('ioredis');
var redis = new Redis(process.env.REDIS_URL);

async function resolveAccountWithRedis(accountNumber, bankCode) {
  var cacheKey = 'resolve:' + bankCode + ':' + accountNumber;

  // Check Redis first
  var cached = await redis.get(cacheKey);
  if (cached) {
    return { account_name: cached, from_cache: true };
  }

  // Check database
  var dbCached = await db.query(
    'SELECT account_name FROM account_resolution_cache ' +
    'WHERE account_number = $1 AND bank_code = $2 AND expires_at > NOW()',
    [accountNumber, bankCode]
  );

  if (dbCached.rows.length > 0) {
    // Found in DB, populate Redis for next time
    await redis.setex(cacheKey, 86400, dbCached.rows[0].account_name); // 24h in Redis
    return { account_name: dbCached.rows[0].account_name, from_cache: true };
  }

  // Call Paystack API
  var result = await resolveFromPaystack(accountNumber, bankCode);

  // Store in both Redis and database
  await redis.setex(cacheKey, 86400, result.account_name);
  await storeInDatabase(accountNumber, bankCode, result.account_name);

  return { account_name: result.account_name, from_cache: false };
}

Redis TTL vs database TTL: Use a shorter TTL in Redis (24 hours) and a longer one in the database (30 days). Redis serves as a fast lookup layer. The database is the authoritative cache. If Redis expires, the next lookup hits the database (still fast) rather than Paystack.

When to skip Redis: If you process fewer than 100 identity lookups per hour, a database-only cache is sufficient. Redis adds infrastructure complexity. Only add it when database query latency becomes a bottleneck.

Throttling Outgoing Requests

Even with caching, you may have bursts of new (uncached) lookups. A request queue prevents these bursts from hitting rate limits.

// throttle.js
var queue = [];
var processing = false;
var DELAY_MS = 200; // 5 requests per second max

function enqueueResolve(accountNumber, bankCode) {
  return new Promise(function(resolve, reject) {
    queue.push({ accountNumber: accountNumber, bankCode: bankCode, resolve: resolve, reject: reject });
    processQueue();
  });
}

async function processQueue() {
  if (processing || queue.length === 0) return;
  processing = true;

  while (queue.length > 0) {
    var item = queue.shift();
    try {
      var result = await resolveAccountCached(item.accountNumber, item.bankCode);
      item.resolve(result);
    } catch (error) {
      item.reject(error);
    }

    // Delay between API calls (only for cache misses)
    if (queue.length > 0) {
      await new Promise(function(r) { setTimeout(r, DELAY_MS); });
    }
  }

  processing = false;
}

200ms between calls gives you roughly 5 requests per second. This is well within typical rate limits while still being fast enough for interactive use. For batch operations (resolving hundreds of accounts during a data import), increase the delay to 500ms or more.

The queue is transparent to callers. Code that calls enqueueResolve gets a Promise back and does not know whether the result came from cache or from a queued API call. The throttling is invisible.

Cache Invalidation

Stale cache data causes real problems. If a user's bank account name has changed and your cache still shows the old name, name matching will fail even though the user entered everything correctly.

User-triggered invalidation. Provide a "Re-verify my account" button that bypasses the cache and calls Paystack directly. This is the most important invalidation mechanism.

// invalidate.js
async function forceResolve(accountNumber, bankCode) {
  // Delete from Redis
  var cacheKey = 'resolve:' + bankCode + ':' + accountNumber;
  await redis.del(cacheKey);

  // Delete from database
  await db.query(
    'DELETE FROM account_resolution_cache WHERE account_number = $1 AND bank_code = $2',
    [accountNumber, bankCode]
  );

  // Call Paystack fresh
  return await resolveAccountCached(accountNumber, bankCode);
}

Time-based expiration. The 30-day TTL on the database cache handles routine staleness. After 30 days, the next lookup will call Paystack fresh and update the cache.

Event-triggered invalidation. If a user changes their bank details on your platform, invalidate the cache for their old account and resolve the new one immediately.

Never invalidate BIN cache. BIN data is permanent. There is no scenario where you need to refresh it.

Handling 429 Responses

When you do hit a rate limit despite caching, handle it gracefully.

// handle-429.js
async function resolveWithRateLimit(accountNumber, bankCode) {
  try {
    return await resolveAccountCached(accountNumber, bankCode);
  } catch (error) {
    if (error.response && error.response.status === 429) {
      // Log the rate limit hit
      console.warn('Paystack rate limit hit. Queuing for retry.');

      // Wait and retry once
      await new Promise(function(r) { setTimeout(r, 5000); });

      try {
        return await resolveAccountCached(accountNumber, bankCode);
      } catch (retryError) {
        // Still rate limited - tell the user
        return {
          error: true,
          message: 'Verification is temporarily busy. Please try again in a minute.',
        };
      }
    }

    throw error;
  }
}

Do not retry 429 immediately. Wait at least 5 seconds before retrying. If you retry instantly, you will get another 429.

Monitor 429 frequency. If you see 429 responses regularly, your caching is not working well enough. Investigate: are you caching results properly? Are there code paths that bypass the cache? Is a batch job flooding the endpoint?

Key Takeaways

  • ✓Paystack rate-limits identity endpoints. The exact limits depend on your account and endpoint. When you hit the limit, you get a 429 response.
  • ✓Cache Resolve Account Number results for 30 days. Account holder names change rarely. Store the resolved name, bank code, and timestamp in your database.
  • ✓Cache Resolve Card BIN results indefinitely. A 6-digit BIN always maps to the same bank and card type. This data never changes.
  • ✓Implement a cache-first pattern: check your local cache before calling Paystack. Only call the API on cache misses.
  • ✓Use a request queue to throttle API calls. Even with caching, bursts of new lookups can hit rate limits.
  • ✓Let users manually trigger a cache refresh when their bank details change. Do not force them to wait 30 days for a stale cache to expire.

Frequently Asked Questions

What is the exact rate limit for Paystack identity endpoints?
Paystack does not publicly document exact rate limits, and they may vary by account type and endpoint. Build your integration to work with any limit by implementing proper caching and throttling. If you consistently hit limits, contact your Paystack account manager to discuss your usage patterns.
Can I cache resolved account names in the browser or on the frontend?
Do not cache account names on the frontend. They contain personal data that should stay on your server. Cache them in your backend database or Redis instance, which you control and can secure.
How much database space does the resolution cache need?
Very little. Each row is roughly 100 bytes (account number, bank code, name, timestamps). Even with a million cached resolutions, the table is under 100MB. The BIN cache is even smaller since there are only about 300,000 known BINs globally.
Should I pre-populate the cache with common accounts?
No. Resolve accounts on demand as users submit their details. Pre-populating would require you to have a list of account numbers, which you should not have. Let the cache build naturally from real user activity.

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 Identity Verification: Complete Guide

Paystack gives you three identity endpoints: resolve account number, resolve card BIN, and validate customer (BVN). This guide shows you how to use each one, build a KYC flow around them, and stay within the Kenya DPA and Nigeria NDPR while doing it.

Read more

Validating a Customer Before Payout

Every payout you send is money you cannot get back. Validating the recipient before initiating a transfer is not optional. This guide shows you how to chain Paystack identity APIs into a validation gate that catches wrong accounts, name mismatches, and suspicious requests before a single kobo leaves your account.

Read more

Building a KYC Flow on Paystack Identity APIs

A KYC flow is not a single API call. It is a sequence of verification steps that increase in strength as the user needs more from your product. This guide shows you how to chain Paystack identity APIs into a progressive KYC system with tiers, database tracking, and clear UX at every step.

Read more

Handling Identity Verification Failures Gracefully

Identity verification calls fail. Banks go offline. Networks time out. BVN checks return unexpected errors. Your job is to handle every failure in a way that does not strand the user. This guide covers every failure mode, the right response for each, and how to build a verification flow that stays useful even when upstream systems break.

Read more

Rotating Paystack Secret Keys Without Downtime

Rotating Paystack API keys is necessary after team changes, suspected leaks, or periodic security reviews. The challenge is that Paystack invalidates old keys immediately. This guide covers strategies to rotate keys without dropping a single payment.

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.