Open banking API rate limits: protect the core without creating an obstacle
Can banks rate limit TPPs under PSD2? Nothing forbids it, but open banking API rate limits must be sized from capacity, applied evenly to TPPs and evidenced.
- open-banking
- compliance
- rate-limiting
- banking
Every ASPSP platform team ends up in the same meeting. An aggregator's refresh job hits the accounts API at the top of the hour, the core banking adapter's connection pool saturates, and the bank's own app slows down with it. Operations wants a hard cap on TPP traffic. Compliance points out that the dedicated interface must perform at least as well as customer channels, and that anything making a TPP's life harder can be read as an obstacle. Both are right, and open banking API rate limits are where they collide.
So can banks rate limit TPPs under PSD2? Yes. Nothing in the RTS forbids throttling. What the rules forbid is a dedicated interface that performs worse than the customer interface, treats TPPs differently without an objective reason, or puts friction in their path. A limit is easiest to defend when it is sized from documented capacity, applied evenly to every TPP in a role, answered with an attributable 429, and backed by evidence that it did not degrade TPP performance.
Why the usual gateway rate limit fails the obstacles test
Every mainstream gateway can throttle. Kong has rate limiting plugins, Apigee has Spike Arrest and Quota, AWS API Gateway ties throttling to usage plans and API keys, Azure API Management has rate-limit and quota policies, and MuleSoft offers SLA-tier rate limiting. The mechanism is not the problem.
The problem is where the numbers come from and where the evidence lives. TPP limits are usually copied from a partner tier template, keyed on an API key rather than a regulated entity, and owned by a team that never sees the quarterly availability statistics. When a TPP complains, nobody can show why the number is what it is or whether TPP response times tracked the customer interface. Commercial tiering, the model behind API monetization, is the wrong template: regulated access is not a product plan.
The quieter failure is the daily quota. A quota that runs out at 18:00 locks a TPP's customers out of their own data until the window resets, while the bank's app keeps working.
What the rules say about open banking API rate limits
Article 32 of the RTS on strong customer authentication and secure communication (Commission Delegated Regulation (EU) 2018/389) is the anchor. A dedicated interface must offer at all times the same availability and performance, including support, as the customer interface, with KPIs at least as stringent, no obstacles to AIS or PIS, and quarterly published statistics for both interfaces.
The EBA Guidelines on the fallback exemption (EBA/GL/2018/07) make that measurable: daily average response time for PIS, AIS and confirmation-of-funds requests, a daily error response rate, statistics TPPs can compare day by day, and stress testing with multiple TPPs, very high request volumes and many concurrent sessions. An exempted bank that falls short of Article 32 for more than two consecutive calendar weeks loses the exemption under Article 33(7).
PSD2 Articles 66(4)(c) and 67(3)(b) require PISP and AISP requests to be treated without discrimination other than for objective reasons. The EBA's 2020 obstacles opinion (EBA/OP/2020/10) names unnecessary registrations and friction-adding redirection, but we have not found EBA material that names rate limiting or throttling. In the UK, the Open Banking Read/Write API profile lets banks throttle beyond a fair usage policy documented in their developer portal, answering with HTTP 429 and, as recommended, Retry-After.
Read together: a limit is capacity management when it is objective, documented, applied evenly and invisible in the performance statistics, and a breach of the Article 32 parity requirement, and likely an obstacle under Article 32(3), when it is why TPP numbers diverge from the customer interface.
Enforce the four-times rule per consent, not per TPP
Article 36(5) lets an AISP access data whenever the customer actively requests it and otherwise no more than four times in 24 hours, unless the AISP and the bank agree a higher frequency with the customer's consent. EBA Q&A 2018_4210 counts four requests, each of which may span several API calls; Q&A 2018_4172 says an automatic refresh when the app opens is not an active request.
The bank learns whether the customer was present only from the TPP's own signal (PSU-IP-Address in Berlin Group, x-fapi-customer-ip-address in the UK standard), and the four-times count belongs to a consent and account, not to a TPP. A Zerq policy counts requests per client across all its profiles, with no notion of consent, account or session, and a 1d quota resets at midnight UTC rather than over a rolling 24 hours. If you enforce Article 36(5)(b), do it in the consent-aware open banking API layer behind Zerq, keyed on consent and the presence signal, not in a gateway policy. Mapping it onto a per-TPP quota would cap all that TPP's customers collectively and block active requests too. If you do not enforce it, monitor unattended polling per TPP and raise patterns through support. Agent features make unattended polling more likely; AI agents in open banking covers giving that traffic its own client and bucket.
The PSR compromise text points the same way. It lets AISPs access data whether or not the customer is actively requesting it, with no four-times cap in the Level 1 text, and requires dedicated interface response times no longer than the customer interface's. As of early October 2026, neither the PSR nor PSD3 is adopted or in force: both procedures show "Awaiting Council's 1st reading position", and article numbers may still change. Do not hard-code today's frequency cap into enforcement.
Where the limit sits in the request path
A TPP request first reaches your TLS ingress. For mtls profiles, certificate validation happens there; for RFC 8705 certificate-bound tokens, the ingress forwards the certificate in the header set by MTLS_CLIENT_CERT_HEADER (typically X-SSL-Client-Cert) and Zerq checks the token's binding to it. Make sure only the ingress can set that header: strip any client-supplied copy and keep the backend off networks where callers could reach it directly.
Inside Zerq, the policy check runs first: the gateway reads X-Client-ID, looks up that client's policy, and answers over-limit traffic with a 429 before anything else runs. It then validates the credential for the profile's auth type (usually oidc) and applies IP, method and collection access rules. Because the policy bucket is keyed on that header before the credential is checked, have the ingress set or verify X-Client-ID for TPP traffic, so no caller can spend another TPP's budget, and strip any caller-sent X-Proxy-ID header, which Zerq treats as a test-console call and leaves out of Request Logs. Rejections (401, 403, 404, 405, 429) come from the gateway itself. Only a request that clears every check runs the proxy's workflow, if any, and reaches your open banking API layer and the core banking adapters behind it. Requests that pass the policy check are logged asynchronously to Request Logs, which feed the dashboard; policy 429s are issued before that logger runs, which is why the next section makes the ingress your record of them.
Two consequences follow. A policy 429 never costs your core a connection. And because a policy attaches to a client, not a route, the client is your unit of fairness. Model one client per TPP, as our open banking setup does; our TPP onboarding guide shows how that client and its profiles are created. If a TPP holds AIS and PIS permissions and needs separate budgets, give it two clients, each assigned only its role's collections. Keep sandbox load off the production bucket, either with a separate sandbox client or by rate-limiting the sandbox host at the ingress, since all profiles under one client share one bucket.
Sizing per-TPP policies from documented capacity
Start from upstream capacity, not round numbers. The stress test the guidelines expect tells you the sustained rate the accounts service handles within its latency target. Size the per-TPP budget for a role from the highest peak demand measured for any single TPP in that role, plus headroom, on the same footing as customer-channel demand. If customer and TPP demand together do not fit, add capacity rather than shrink the TPP share, because Article 32(1) and PSD2 Articles 66(4)(c) and 67(3)(b) leave no room for prioritising your own channels. Create the policy under Policies in the console, or through the management API (writes need the modifier or admin role):
# One policy per TPP role, shared by every AISP client.
# Interval: 1m, 5m or 1h. A 5m window absorbs short refresh bursts.
# No quota fields, so no daily lockout (1d quotas reset at 00:00 UTC).
curl -X POST https://zerq-admin.bank.internal/api/v1/policies \
-H "Authorization: Bearer $OIDC_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ob-aisp-standard-2026q4",
"description": "AIS role. Sized from accounts stress test ST-2026-09: peak per-TPP AIS demand plus 30% headroom, within total capacity shared with customer channels. Review quarterly.",
"active": true,
"rate_limit_requests": 1500,
"rate_limit_interval": "5m"
}'
The description carries the capacity reference on purpose: every policy create or update becomes an audit log entry (with AUDIT_ENABLED on) with actor ID, action, timestamp and the new values in the request body. Give review to people who hold only the read-only auditor role (the default config also grants it to admin). For who can change limits and who can review those changes, see the security model. Attach the policy by opening each TPP client, clicking Edit and choosing it from the Policy dropdown.
Then confirm which limit a TPP is actually under. Policy enforcement runs in the gateway's limiter middleware, which GATEWAY_ENABLE_LIMITER switches on. With it on, a client with no active policy falls back to the deployment default (GATEWAY_LIMITER_MAX requests per GATEWAY_LIMITER_EXPIRATION), so it is not unlimited. Check both before a new TPP inherits an unsized default.
Making every 429 attributable
A 429 is where a TPP meets your limit, so it should explain itself. Zerq answers a rate-limit rejection with 429 Too Many Requests, a Retry-After header and a JSON body whose error field reads rate_limit_exceeded, and the response carries the gateway's X-Request-ID. Article 36(2) already requires a message explaining unexpected errors, and a 429 that says when to retry answers it better than a bare status code. Publish the per-role numbers in your interface documentation and in the collection description TPPs see in the Developer Portal.
Count 429s per TPP at the ingress as your system of record, because that log sees every status the TPP received, including rejections the ingress issues itself. That count feeds the error response rate you publish and settles disputes about a rejection. Log the X-Client-ID request header and the gateway's X-Request-ID response header at the ingress, so a request ID a TPP quotes can be matched there.
Proving limits did not degrade TPP performance
The evidence question is comparative: did TPPs get the same response times and success rates as the customer interface on the same services? If your app and web backends call the same collections through Zerq as their own clients, the dashboard answers it:
- Open the dashboard and set Time range to 24h. The 24h preset is a rolling window, so pull it at the same time each day (for example 00:00 UTC) to get a calendar-day figure.
- Set Collection to the service you are checking, for example
accounts. - Set Client to the retail app's client and note Avg Response Time and Success Rate.
- Switch Client to each TPP in turn and record the same two figures.
- For any outlier, open Request Logs filtered by Client ID, Collection and a latency threshold.
The dashboard reports averages, matching the daily average response time the guidelines ask for. If customer channels bypass the gateway, pair Zerq's TPP figures with your channel monitoring.
Parity check: collection "accounts", Time range 24h, pulled 2026-10-08 00:00 UTC
Client filter Total Requests Success Rate Avg Response Time
retail-app-bff 412,880 99.7% 188 ms
tpp-aisp-0042 96,114 99.6% 191 ms
tpp-aisp-0107 41,702 99.4% 612 ms <- investigate
Request Logs: Client ID = 66f2b1c0a9e4d3f7b8c6a512 (tpp-aisp-0107), Collection = accounts, Latency > 1000
Request ID 7c1e9a4f-2b6d-4c55-9e0a-3f8d21b0c6aa
Timestamp 2026-10-07T05:58:12Z
Method GET
Path /ob/aisp/v1/accounts/acc-77812/transactions
Target endpoint https://core-adapter.bank.internal/accounts/transactions
Status code 200
Latency 2930
Client ID 66f2b1c0a9e4d3f7b8c6a512
Profile ID 66f2b1c0a9e4d3f7b8c6a513
Collection accounts
The outlier is not a throttling problem. Its slow entries are served 200 responses from the core adapter's transactions endpoint, not rejections, so correlate with the adapter's own metrics and fix it upstream, not in the policy.
A decision checklist for defensible TPP limits
- Is every limit derived from a documented capacity measurement referenced in the policy description?
- Does every TPP in a role get the same policy, with differences traceable to objective reasons?
- Is there any daily quota that could lock a TPP out while customer channels keep working?
- Is the four-times rule handled per consent in the API layer or through monitoring, rather than as a per-TPP gateway cap?
- Do you know whether each TPP client is under its own policy or the deployment-wide limiter?
- Does the 429 carry
Retry-Afterand a request ID the TPP can quote? - Are limits published in your interface documentation, with tightening routed through your Article 30(4) change notice process?
- Can you produce, per day and per service, TPP response times and success rates next to the customer interface's, with 429s counted?
What this looks like in practice
Consider a retail bank that runs TPP traffic through one key-based limit of 100 requests per minute, copied from its partner API program. An aggregator's hourly refresh trips it, the aggregator's customers see stale balances, and the aggregator escalates. The bank cannot say how the number was chosen or how TPP latency compared with the app that week.
If the bank moves the dedicated interface onto Zerq, each TPP becomes its own client under one of two role policies sized from its September stress test, with no daily quotas. The numbers go into the interface documentation, operations pulls the parity comparison per collection at 00:00 UTC each day, and someone holding only the auditor role reviews policy changes in Audit Logs. When the aggregator's pattern returns, the 5m window absorbs the burst, the ingress 429 count for that TPP shows whether any request was rejected, and the parity comparison shows whether its response times tracked the app's. For burst isolation and retry storms generally, see rate limits that protect upstreams without punishing partners.
Rate limits as evidence, not just enforcement
TPP rate limiting is defensible when it reads as capacity management in every artifact a supervisor might ask for: a number derived from a stress test, one policy per role, a 429 that explains itself, an audited change history, and daily evidence that dedicated interface performance tracked the customer interface. Keep the four-times rule out of per-client gateway policies, avoid quotas that lock TPPs out, and treat the parity comparison as the output that matters.
Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.