MT5 API rate limits: concurrency and retries that stay bounded

5 min readMetaKit

An MT5 reporting script can work for one account and struggle at twenty without a single endpoint changing. The usual cause is the request pattern: every account starts its history download, position refresh and performance calculation at the same instant. Adding automatic retries makes the burst larger.

A reliable client needs separate budgets for request rate, concurrent work and retries. This guide develops that client-side policy for read-only reporting through the MetaKit REST API. It does not prescribe an order-execution retry policy; that has different consequences and is covered in the idempotency guide.

What limit does MetaKit actually publish?

As checked on 18 October 2026, the MetaKit API reference does not publish a fixed requests-per-second quota. That is not an unlimited-capacity promise. Calls can reach a real terminal, and account-level reads can perform several underlying operations.

Do not borrow another provider's quota and present it as an MT5 platform limit. A reverse proxy, hosting layer or future API policy can also impose constraints independently of the terminal. Treat the response you receive and the current provider documentation as the relevant contract.

The current performance handler shares an approximately 60-second deal-history cache per account. The raw /v1/accounts/{id}/deals handler instead calls history with the requested dates directly. These are different paths: a cache on performance does not imply that every historical query is cached. Avoid using a reporting refresh interval as evidence that the underlying positions or quotes are fresh.

Rate and concurrency solve different problems

Rate means how many requests start within a period. Concurrency means how many remain in progress. Four workers can generate forty requests per second if each finishes in a tenth of a second. Conversely, two slow history calls can occupy both workers for several seconds despite a low request rate.

For an illustrative workload of twenty accounts with three reads each, there are sixty requests. At a hypothetical average of 0.3 seconds per request, sequential work takes roughly eighteen seconds. Four continuously busy workers could finish in about 4.5 seconds under ideal conditions. These are arithmetic estimates, not measured MetaKit latency or recommended service quotas.

Start with a small global worker pool and serialize expensive history jobs for each account. Add a start-rate pacer if bursts remain a problem. If you run three application replicas, a four-worker pool in each allows twelve concurrent calls overall. Shared limits require coordination outside an individual process.

A bounded read retry helper

HTTP 429 means too many requests; the response may supply Retry-After. The HTTP specification permits that header to contain either delay seconds or an HTTP date. Handle both rather than assuming it is always a number.

This Node.js example accepts a trusted MetaKit URL constructed by your application. Keep the bearer token server-side. It retries GET requests only, with at most three total attempts. Its delay budget deliberately refuses long waits so the scheduler can retry the job later.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 
function retryAfterMs(value, now = Date.now()) {
  if (value === null) return null;
  const text = value.trim();
  if (/^\d+$/.test(text)) return Number(text) * 1000;
  const date = Date.parse(text);
  return Number.isFinite(date) ? Math.max(0, date - now) : null;
}
 
async function readJson(url, token, fetcher = fetch, pause = sleep) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const response = await fetcher(url, {
      method: "GET",
      headers: { Authorization: `Bearer ${token}` },
      signal: AbortSignal.timeout(8000),
      redirect: "error",
    });
    if (response.ok) return response.json();
    const retryable = [429, 502, 503, 504].includes(response.status);
    const instructed = retryAfterMs(response.headers.get("retry-after"));
    await response.body?.cancel();
    if (!retryable || attempt === 2) {
      throw new Error(`GET failed: HTTP ${response.status}`);
    }
    const delay = instructed ?? Math.random() * (500 * 2 ** attempt);
    if (!Number.isFinite(delay) || delay > 5000) {
      throw new Error("Retry deferred: wait exceeds this job's budget");
    }
    await pause(delay);
  }
}

The helper uses a fresh timeout signal for each attempt. Transport exceptions, malformed successful JSON and timeouts propagate to the caller instead of automatically restarting the loop. That is an intentional conservative boundary for this small example. A production scheduler should classify those failures and record them without creating an immediate retry storm.

Three eight-second attempts plus two waits of at most five seconds give approximately 34 seconds of request-and-wait budget, excluding other processing. Set a separate whole-job deadline if your scheduler needs one. Never shorten a valid server-requested wait just to squeeze another request into that deadline: defer the job instead.

Limit work before starting promises

Calling Promise.allSettled(accounts.map(fetchAccount)) preserves individual outcomes, but starts every request immediately. The aggregation method is not a concurrency limiter. MDN describes allSettled as collecting promise outcomes; it does not schedule a worker pool for you.

Keep the queue outside the fetch helper. A worker takes one account, performs its reads, records a successful result or an explicit failure, then takes the next account. Do not replace a failed account with zero equity: that converts an availability problem into a misleading financial result.

Separate freshness classes as well. A live exposure panel, a daily report and a multi-year backfill should not share the same polling cadence. Backfills can yield capacity to interactive reads. Use events to trigger work where appropriate, with periodic reconciliation as described in the webhook reliability guide.

Measure before increasing the budget

Record queue wait, request duration, attempts, HTTP status and the age of the data returned. Aggregate by endpoint class and account rather than logging credentials or full financial payloads. A rising queue with stable successful request times suggests insufficient scheduling capacity; rising request times and errors suggest adding workers may make things worse.

For local verification, mock a 429 followed by a success, both forms of Retry-After, a delay above the budget, a non-retryable 401, three consecutive upstream failures and a transport exception. Then test the worker queue's maximum active count. The helper's cases were checked with synthetic responses; no live trading requests are needed to validate the policy.

The useful target is a client that completes reports predictably and makes partial failures visible. For the actual account connection flow, start with the Node.js integration walkthrough and confirm endpoint behavior in the API documentation.

Sources and verification

Verified 18 October 2026. Product details were checked against MetaKit's current route handlers and public reference. Examples and workload estimates are illustrative.