IBAN Validation at Scale: Performance Patterns for High-Volume APIs
Performance patterns for high-volume IBAN validation: local MOD-97 pre-filtering, micro-batching, two-layer caching, and concurrency limiting.
Founder, ibanchecker.cash
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",
"Authorization": `Bearer ${process.env.IBANCHECKER_API_KEY}`,
},
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",
"Authorization": `Bearer ${process.env.IBANCHECKER_API_KEY}`,
},
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",
"Authorization": `Bearer ${process.env.IBANCHECKER_API_KEY}`,
},
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.
Sources & References
- SWIFT IBAN Registry: Official IBAN country format specifications
- ISO 13616-1:2020, International standard for IBAN structure
- EPC: Registers of participants in SEPA payment schemes
Last updated: June 2026
Integrate IBAN validation via API
REST API with 100 free validations a month. JSON response with bank name, SEPA status, and BBAN breakdown.
Introducing Northern Cyprus UBAN Support: Validator and Bank Directory
A validator for the 28-character Northern Cyprus UBAN, plus a directory of all 23 registered banks. What UBAN is, and wh...
One Client, Four Registries: PyPI, npm, Packagist and MCP All Disagree
Why OIDC cannot do npm's first publish, why Packagist needs no workflow, and the PyPI name check that returns 200 for na...
npm trust github Fails with 400 Bad Request: The Missing Permission Flag
A bare 400 with no error body, caused by a 20 May 2026 registry rule. The exact npm version boundary, the one flag that ...
How to Validate IBANs in JavaScript (API & Manual)
Two approaches: call the ibanchecker.cash API for full bank data, or implement MOD-97 validation yourself. Includes work...