ibanchecker.cash
Developer ResourcesJune 13, 2026 · 10 min read

IBAN Validation at Scale: Performance Patterns for High-Volume APIs

Performance patterns for high-volume IBAN validation: local MOD-97 pre-filtering, micro-batching with the bulk API endpoint, two-layer LRU + Redis caching, concurrency limiting, and observability metrics.

Share

IBAN validation at scale introduces challenges that do not exist when validating a handful of IBANs per second: rate limit management, latency variance, cache invalidation, and cost optimization. This guide covers the architecture patterns used by fintech platforms that process hundreds of thousands of IBANs per day — including local pre-filtering, intelligent batching, multi-layer caching, and parallel execution with controlled concurrency.

Why Does a Single Validate-Per-Request Architecture Break at Scale?

At low volume — under a few thousand validations per day — calling POST /api/v1/validate inline on each user request works fine. At higher volumes, three problems emerge:

  • Rate limits: The free tier enforces a per-hour cap. Without local pre-filtering, every malformed or obviously invalid string consumes an API call before returning a predictable error.
  • Latency accumulation: A synchronous validate call adds 50–150 ms to every user request. At 50 requests per second, this becomes a dominant cost in your p99 response time.
  • Redundant work: In many systems, the same IBAN is validated multiple times — on form submission, on record save, on export, and during nightly reconciliation. Without caching, each check is a separate API call.

How Do You Pre-Filter IBANs Before Hitting the API?

Run the full MOD-97 algorithm locally — in your application process — before sending any IBAN to the external API. Local validation is synchronous, has zero network overhead, and eliminates all obviously invalid inputs from your API quota. Depending on your input quality, this alone can cut API call volume by 30–70%.

const IBAN_LENGTHS = {
  AD: 24, AE: 23, AL: 28, AT: 20, AZ: 28,
  BA: 20, BE: 16, BG: 22, BH: 22, BR: 29,
  BY: 28, CH: 21, CR: 22, CY: 28, CZ: 24,
  DE: 22, DK: 18, DO: 28, EE: 20, EG: 29,
  ES: 24, FI: 18, FO: 18, FR: 27, GB: 22,
  GE: 22, GI: 23, GL: 18, GR: 27, GT: 28,
  HR: 21, HU: 28, IE: 22, IL: 23, IQ: 23,
  IS: 26, IT: 27, JO: 30, KW: 30, KZ: 20,
  LB: 28, LC: 32, LI: 21, LT: 20, LU: 20,
  LV: 21, LY: 25, MC: 27, MD: 24, ME: 22,
  MK: 19, MR: 27, MT: 31, MU: 30, NL: 18,
  NO: 15, PK: 24, PL: 28, PS: 29, PT: 25,
  QA: 29, RO: 24, RS: 22, SA: 24, SC: 31,
  SE: 24, SI: 19, SK: 24, SM: 27, ST: 25,
  SV: 28, TL: 23, TN: 24, TR: 26, UA: 29,
  VA: 22, VG: 24, XK: 20,
};

function normalizeIban(raw) {
  return raw.trim().replace(/[s-]/g, "").toUpperCase();
}

function localValidate(raw) {
  const iban = normalizeIban(raw);
  if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(iban)) return false;
  const expected = IBAN_LENGTHS[iban.slice(0, 2)];
  if (!expected || iban.length !== expected) return false;
  const rearranged = iban.slice(4) + iban.slice(0, 4);
  const numeric = rearranged.split("").map((c) => c >= "A" ? (c.charCodeAt(0) - 55).toString() : c).join("");
  return BigInt(numeric) % 97n === 1n;
}

async function validateWithPreFilter(iban) {
  if (!localValidate(iban)) {
    return { valid: false, source: "local", error: "Failed local MOD-97 check" };
  }

  const res = await fetch("https://ibanchecker.cash/api/v1/validate", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ iban }),
  });

  const result = await res.json();
  return { ...result, source: "api" };
}

How Do You Batch IBANs to Maximize Throughput?

The POST /api/v1/validate/bulk endpoint accepts up to 100 IBANs per request. At scale, batching is the single highest-impact optimization: 100 IBANs validated in one request versus 100 individual calls reduces network round-trips by 99× and dramatically lowers your API call count.

Implement a micro-batcher that collects incoming IBANs for a short window (5–20 ms) and flushes them as a batch:

class IBANBatcher {
  #queue = [];
  #flushTimer = null;
  #windowMs;

  constructor(windowMs = 10) {
    this.#windowMs = windowMs;
  }

  validate(iban) {
    return new Promise((resolve, reject) => {
      this.#queue.push({ iban, resolve, reject });
      if (!this.#flushTimer) {
        this.#flushTimer = setTimeout(() => this.#flush(), this.#windowMs);
      }
    });
  }

  async #flush() {
    this.#flushTimer = null;
    const batch = this.#queue.splice(0, 100);
    if (batch.length === 0) return;

    try {
      const res = await fetch("https://ibanchecker.cash/api/v1/validate/bulk", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ibans: batch.map((b) => b.iban) }),
      });

      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const { results } = await res.json();
      batch.forEach((item, i) => item.resolve(results[i]));
    } catch (err) {
      batch.forEach((item) => item.reject(err));
    }

    if (this.#queue.length > 0) this.#flush();
  }
}

const batcher = new IBANBatcher(10);

async function validateConcurrent(ibans) {
  return Promise.all(ibans.map((iban) => batcher.validate(iban)));
}

How Do You Cache Validation Results to Avoid Redundant API Calls?

IBAN validity is deterministic — the same IBAN always produces the same MOD-97 result, and bank metadata changes infrequently. A two-layer cache (in-memory LRU for hot IBANs + Redis for cross-instance sharing) eliminates redundant API calls entirely for repeated IBANs.

import { LRUCache } from "lru-cache";
import Redis from "ioredis";

const LOCAL_CACHE = new LRUCache({ max: 10_000, ttl: 24 * 60 * 60 * 1000 });
const redis = new Redis(process.env.REDIS_URL);
const CACHE_TTL_SECONDS = 86400;

async function getCached(iban) {
  const local = LOCAL_CACHE.get(iban);
  if (local) return local;

  const remote = await redis.get(`iban:${iban}`);
  if (remote) {
    const parsed = JSON.parse(remote);
    LOCAL_CACHE.set(iban, parsed);
    return parsed;
  }

  return null;
}

async function setCached(iban, result) {
  LOCAL_CACHE.set(iban, result);
  await redis.setex(`iban:${iban}`, CACHE_TTL_SECONDS, JSON.stringify(result));
}

async function validateCached(raw) {
  const iban = normalizeIban(raw);

  if (!localValidate(iban)) {
    return { valid: false, source: "local" };
  }

  const cached = await getCached(iban);
  if (cached) return { ...cached, source: "cache" };

  const res = await fetch("https://ibanchecker.cash/api/v1/validate", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ iban }),
  });

  const result = await res.json();
  if (result.valid) await setCached(iban, result);
  return { ...result, source: "api" };
}

How Do You Control Concurrency to Avoid Overwhelming the API?

When processing large files or queue backlogs, naive Promise.all() over thousands of IBANs will fire all requests simultaneously, triggering rate limit responses (HTTP 429). Use a concurrency limiter to keep in-flight requests within the API's sustainable throughput window.

async function validateWithConcurrencyLimit(ibans, concurrency = 10) {
  const results = new Array(ibans.length);
  const executing = [];

  for (let i = 0; i < ibans.length; i++) {
    const task = (async () => {
      results[i] = await validateCached(ibans[i]);
    })();

    executing.push(task);

    if (executing.length >= concurrency) {
      await Promise.race(executing);
      executing.splice(executing.findIndex((p) => p === task), 1);
    }
  }

  await Promise.all(executing);
  return results;
}

const ibans = Array.from({ length: 5000 }, (_, i) => `DE${String(i).padStart(20, "0")}`);
const results = await validateWithConcurrencyLimit(ibans, 10);
console.log(`Validated ${results.length} IBANs`);

How Do You Monitor and Observe Validation Performance?

Track four metrics to maintain SLAs and spot degradation early:

  • Cache hit rate: Should be above 60–80% in systems where IBANs recur (supplier databases, recurring payroll). A low hit rate indicates IBANs are being validated redundantly.
  • Local reject rate: The fraction of IBANs that fail MOD-97 locally before reaching the API. High reject rates suggest upstream data quality issues.
  • API p99 latency: Track the tail latency, not just the mean. p99 spikes often indicate rate limit backpressure or network issues.
  • 429 rate: Any rate limit responses indicate your concurrency settings are too high or your batch window is too short.
const metrics = {
  total: 0,
  cacheHits: 0,
  localRejects: 0,
  apiCalls: 0,
  rateLimitHits: 0,
};

async function validateTracked(iban) {
  metrics.total++;
  const result = await validateCached(iban);

  if (result.source === "local") metrics.localRejects++;
  else if (result.source === "cache") metrics.cacheHits++;
  else metrics.apiCalls++;

  return result;
}

setInterval(() => {
  const { total, cacheHits, localRejects, apiCalls } = metrics;
  if (total === 0) return;
  console.log({
    cacheHitRate: ((cacheHits / total) * 100).toFixed(1) + "%",
    localRejectRate: ((localRejects / total) * 100).toFixed(1) + "%",
    apiCallRate: ((apiCalls / total) * 100).toFixed(1) + "%",
  });
}, 60_000);

For high-volume deployments, upgrade to a paid plan to get higher rate limits and a dedicated SLA. See the pricing page and the API documentation for details.

Last updated: June 2026

Validate an IBAN instantly

Free IBAN checker — MOD-97 verification, bank lookup, and SEPA status across 84 countries.

Open IBAN Checker →

Related Articles