Hire Me
← All Writing Betfair

API Rate Limiting — Managing Quotas in High-Frequency Systems

How to manage Betfair API rate limits in a Java trading system — data request quotas, charge model, request batching, the Streaming API, and monitoring quota usage in production.

The first time you hit a Betfair API rate limit in a production system, it tends to happen at the worst possible moment. You’ve been testing with a handful of markets, everything works fine, and then on a busy Saturday with 40 concurrent races you start seeing TOO_MUCH_DATA errors in your logs and your price feed goes dark on half your markets. I’ve been running high-frequency trading frameworks on the Betfair API for years, and rate limit management is one of the first things you need to get right before you can rely on the system at scale.

How Betfair’s Data Quotas Work

Betfair operates two separate rate limit systems, and confusing them is a common source of problems.

Application-level data request quota — this limits how much market data you can retrieve per hour across all your API calls. The quota is measured in “data points” — roughly speaking, the number of price levels you request across all market books in a given time window. Requesting the full ladder (the EX_ALL_OFFERS price projection) across many markets consumes far more quota than requesting best-offers only.

Account-level request throttle — this limits raw request rate. For the exchange betting API, the standard limit is approximately 5 requests per second for free API keys, rising to higher limits for vendor keys. Sustained bursts above this trigger EXCEEDED_THROTTLE errors.

The exact quota figures depend on your application key tier. Check your current usage and limits via getDeveloperAppKeys — this returns your remaining allowance and resets at the hour boundary.

@Service
@RequiredArgsConstructor
public class BetfairQuotaMonitor {

    private final BetfairRestClient client;
    private final MeterRegistry meterRegistry;

    @Scheduled(fixedDelay = 60_000)
    public void reportQuotaUsage() {
        try {
            DeveloperApp app = client.getDeveloperAppKeys().stream()
                .findFirst()
                .orElseThrow();

            app.appVersions().forEach(version -> {
                meterRegistry.gauge("betfair.quota.remaining",
                    Tags.of("key_type", version.ownerType()),
                    version.subscriptionTokenInfo().quotaRemaining());
            });
        } catch (Exception e) {
            log.warn("Failed to retrieve Betfair quota info: {}", e.getMessage());
        }
    }
}

Expose this as a metric and alert before you hit 20% remaining, not after you hit zero.

Minimising listMarketBook Calls

listMarketBook is the highest-frequency call in most polling-based systems, and it’s the primary driver of quota consumption. Three techniques reduce its cost significantly.

Batch multiple markets into a single call. The API accepts up to 200 market IDs per listMarketBook request. Instead of one call per market, batch your active market IDs:

@Service
@RequiredArgsConstructor
public class MarketBookPoller {

    private static final int BATCH_SIZE = 40;
    private final Set<String> activeMarketIds = ConcurrentHashMap.newKeySet();
    private final BetfairRestClient client;

    @Scheduled(fixedRate = 1000)
    public void poll() throws Exception {
        List<String> marketIds = new ArrayList<>(activeMarketIds);

        // Split into batches to stay within the 200-market limit
        Lists.partition(marketIds, BATCH_SIZE).forEach(batch -> {
            try {
                List<MarketBook> books = client.listMarketBook(
                    buildRequest(batch, PriceData.EX_BEST_OFFERS));
                books.forEach(this::process);
            } catch (Exception e) {
                log.error("listMarketBook batch failed: {}", e.getMessage());
            }
        });
    }

    private ListMarketBookRequest buildRequest(List<String> ids, PriceData priceData) {
        PriceProjection projection = new PriceProjection();
        projection.setPriceData(Set.of(priceData));

        ListMarketBookRequest request = new ListMarketBookRequest();
        request.setMarketIds(ids);
        request.setPriceProjection(projection);
        return request;
    }
}

Use EX_BEST_OFFERS not EX_ALL_OFFERS unless you need the full ladder. Best-offers returns only the three best back and lay prices. Full ladder returns up to ten levels on each side. If your strategy only needs mid-price or best-price signals, EX_BEST_OFFERS consumes roughly 85% less quota per market.

Stop polling markets you don’t need. This sounds obvious but it’s easy to accumulate active market IDs in a set that never gets pruned. Implement explicit lifecycle management — add a market when you decide to trade it, remove it once it’s settled or you’ve exited:

public void deactivateMarket(String marketId) {
    activeMarketIds.remove(marketId);
    log.info("Deactivated polling for market {}, {} markets now active",
        marketId, activeMarketIds.size());
}

Request Throttling with a Rate Limiter

Even with batching, you need to rate-limit outbound requests to stay under the account-level throttle. The Resilience4j RateLimiter is the cleanest way to do this in Spring Boot:

@Configuration
public class BetfairRateLimiterConfig {

    @Bean
    public RateLimiter betfairRateLimiter() {
        RateLimiterConfig config = RateLimiterConfig.custom()
            .limitForPeriod(4)               // 4 requests per second
            .limitRefreshPeriod(Duration.ofSeconds(1))
            .timeoutDuration(Duration.ofSeconds(5))
            .build();
        return RateLimiter.of("betfair-api", config);
    }
}

@Service
@RequiredArgsConstructor
public class ThrottledBetfairClient {

    private final BetfairRestClient delegate;
    private final RateLimiter rateLimiter;

    public List<MarketBook> listMarketBook(ListMarketBookRequest request) throws Exception {
        return RateLimiter.decorateCheckedSupplier(rateLimiter,
            () -> delegate.listMarketBook(request)).get();
    }
}

Set limitForPeriod slightly below the documented limit — Betfair’s throttle enforcement has some tolerance, but running at exactly the limit means any burst of latency will push you over it.

The Streaming API — The Right Answer for Price Data

If your system needs sub-second price updates on more than a few markets, stop polling listMarketBook and use the Betfair Streaming API instead.

The Streaming API is a persistent TCP connection that pushes delta updates in real time. It has no per-update quota cost — it does not count against your data request quota at all. Market updates arrive within milliseconds of being matched. A single connection can receive updates for hundreds of markets simultaneously.

The trade-off is implementation complexity: you maintain a persistent connection, handle reconnection on disconnect, apply delta updates to an in-memory market state, and manage subscription scope carefully to avoid receiving updates for markets you don’t care about.

@Component
@Slf4j
public class BetfairStreamSubscription {

    private static final String STREAM_HOST = "stream-api.betfair.com";
    private static final int STREAM_PORT = 443;

    private volatile boolean running = false;

    public void subscribe(List<String> marketIds, MarketDataHandler handler) {
        running = true;

        // Subscribe to specific markets with a BestAvailableToBack/Lay filter
        MarketFilter filter = new MarketFilter();
        filter.setMarketIds(new HashSet<>(marketIds));

        MarketDataFilter dataFilter = new MarketDataFilter();
        dataFilter.setFields(Set.of("EX_BEST_OFFERS", "EX_TRADED"));
        dataFilter.setLadderLevels(3);

        // Connection and reconnection logic omitted for brevity —
        // see the betfair-aping-demo or betfairlightweight (Python) as reference
        // implementations for the full protocol
        log.info("Streaming subscription started for {} markets", marketIds.size());
    }
}

The rule I apply in my own framework: if a market needs price updates more frequently than every two seconds, it goes on the Streaming API. If it’s a lower-priority market for background monitoring, polling at a low rate is fine and simpler to operate.

Monitoring API Usage in Production

Beyond quota metrics, instrument every outbound Betfair call for latency and error rate:

public List<MarketBook> listMarketBook(ListMarketBookRequest request) throws Exception {
    Timer.Sample sample = Timer.start(meterRegistry);
    try {
        List<MarketBook> result = delegate.listMarketBook(request);
        sample.stop(meterRegistry.timer("betfair.api.calls",
            "operation", "listMarketBook", "status", "success"));
        return result;
    } catch (Exception e) {
        sample.stop(meterRegistry.timer("betfair.api.calls",
            "operation", "listMarketBook", "status", "error",
            "error_type", e.getClass().getSimpleName()));
        throw e;
    }
}

Build a dashboard that shows calls per minute, error rate, p95 latency, and remaining quota side by side. When quota starts dropping unusually fast, the call-rate metric usually tells you which operation is responsible.

ProTips From Running This in Production

Never share an application key across multiple processes. If you run two instances of your trading service and both use the same app key, their quota consumption is pooled and their request rates are summed. Each process should have its own key.

Handle TOO_MUCH_DATA with exponential backoff, not immediate retry. An immediate retry after a quota error burns more quota. Back off for 30 seconds, log the event, and check whether your request pattern is sustainable.

The lastMatchTime field in MarketBook tells you whether the market has traded since your last call. For markets with low trading volume, you can skip re-processing the book if lastMatchTime hasn’t changed. Not a Betfair API feature — just a field you can exploit.

Betfair’s API can return 200 OK with an error body. Always check the response status field, not just the HTTP status code. A request that exceeds quota returns HTTP 200 with an error in the JSON body.

If you’re building a Java trading system on the Betfair API and need reliable, production-grade quota management, get in touch.