PythonccxtOKXCloudflareTrading

Why your bot gets a 403 from Cloudflare (and how to harden a ccxt client)

Published on 2026-07-17 · Xiliux

If you automate an exchange with ccxt, sooner or later you'll see it in the logs: short bursts of 403 Forbidden hitting fetch_balance, the OHLCV calls or the earn balance, which clear up on their own after a few minutes. It's not that your API key is wrong. It's the WAF (Cloudflare) that many exchanges put in front of their REST API, challenging something that "looks like a bot".

And your bot is a bot — but a legitimate one, operating your own account against the official API. The problem isn't permissions, it's HTTP client reputation. This is about reducing the WAF's false positives, not evading any access control.

Two layers that mitigate it

I pulled this pattern out of my own OKX bot after several bouts of 403s, and released it as a library: ccxt-resilience (Apache-2.0).

1. Make the WAF challenge less: harden

A default ccxt client announces itself as what it is. Setting a browser User-Agent, the Accept-Language header and a generous timeout makes Cloudflare challenge it less often:

import ccxt
from ccxt_resilience import harden

exchange = harden(ccxt.okx({
    "apiKey": ...,
    "secret": ...,
    "password": ...,
}))

harden acts on an already-built client, returns the same object (chainable) and never breaks its construction: if setting an attribute fails, it leaves it as it was.

2. Retry only what you should: with_retry

The temptation is to wrap everything in a try/except that retries. It's a trap: retrying a credentials or funds error only wastes time, ends just as badly, and hides logic bugs behind waits.

The key is to retry only the transient — 403/Cloudflare, 429, timeouts — with exponential backoff and jitter, and re-raise the real errors on the spot:

from ccxt_resilience import with_retry

balance = with_retry(exchange.fetch_balance)

ohlcv = with_retry(exchange.fetch_ohlcv, "BTC/USDT", timeframe="1m",
                   attempts=4, base=1.0, max_s=8.0)

An authentication error is re-raised immediately, without retrying. And if the attempts run out, the last exception is re-raised, so your fail-safe handler stays in charge (for example, returning the last cached value).

The wait for attempt i is min(max_s, base * 2**i) + rand()*base: it grows in a bounded way and the jitter keeps multiple clients from retrying in sync.

The classification is explicit

What makes an exception retryable or not is not hidden: it's a function you can inspect and replace.

from ccxt_resilience import is_transient_error

# A Cloudflare 403? Yes. An invalid key? No.
is_transient_error(Exception("403 Forbidden Cloudflare"))   # True
is_transient_error(Exception("invalid api key"))            # False

With ccxt installed it also classifies by exception type (DDoSProtection, RequestTimeout…); without it, by the message. That's why ccxt is a soft dependency: the library works with or without it. And if your case is a different API, with_retry accepts your own retry predicate.

Installation

It's free software. Since August 2026 the code is not published to registries or on GitHub — we work in security, and having our source extracted would be an argument against the product —: it's delivered on request, signed and with a SHA-256, at contacto@xiliux.com. The ccxt-resilience repo has the description and how to request it. It's small, stateless infrastructure; use it piece by piece.

FAQ

Is a 403 from Cloudflare a permissions problem?

No. Your API key is fine; the WAF challenges your HTTP client's reputation, not your account. It's a false positive from the anti-bot filter that clears itself in minutes. This is about reducing those false positives, not about bypassing any access control.

Is hardening the client 'evading' Cloudflare?

No. You're a legitimate bot operating your own account against the official API. Setting a browser User-Agent and Accept-Language just makes a legitimate client look less like scraper noise. You don't solve a CAPTCHA or bypass a control — you lower false positives.

What exactly should the retry retry?

Only transient failures — 403/429/timeout — with exponential backoff, and re-raise the real errors (bad parameters, auth) instantly. Retrying everything hides real bugs; retrying nothing lets a transient WAF challenge take down your loop.

How do I add it to an existing ccxt client?

harden() wraps an ALREADY-built ccxt client, returns the same object (chainable) and never breaks its construction: if it can't set a field, it leaves the client working. You keep your setup and gain resilience.

← More articlesRequest a quote