Skip to navigation

Rate limits

The limits on submitting work, how a response advertises them, and when they reset.

The policies

Three limits apply to the calls that spend them. Each is per environment: every key of your live environment shares the live limits, and your test environment has its own.

PolicySpent byWindowResets
submiteach job submission (202)60 scontinuously, as the window passes
uploadeach asset upload (201)60 scontinuously, as the window passes
dailyeach admitted job86400 sat 00:00 UTC

The numbers depend on your plan. A limit can change while you are using the API, for example when your plan changes; the new value applies within about a minute. Reads — polling a job, fetching a result, usage and balance — are not rate limited and carry no rate-limit headers.

A refusal is a 429 with one of these codes:

RATE_LIMITED the submit or upload policy is spent; retry after the reset
QUOTA_EXCEEDED the daily policy is spent; it resets at 00:00 UTC
TENANT_QUEUE_LIMIT_EXCEEDED too many of your jobs are waiting; retry after Retry-After

The headers

Rate-limit headers are turned on per account. Until they are on for yours, responses carry none of them, and every limit below is still enforced the same way. Handle 429 and Retry-After whether or not you see the headers.

A response that spent a policy carries the IETF structured pair (draft-ietf-httpapi-ratelimit-headers): RateLimit-Policy names each policy’s quota q per window w seconds, and RateLimit names what remains, r, and the seconds until the policy is whole again, t.

HTTP/1.1 202 Accepted
RateLimit-Policy: "submit";q=60;w=60, "daily";q=1000;w=86400
RateLimit: "submit";r=59;t=1, "daily";r=412;t=30791
RateLimit-Limit: 60
RateLimit-Remaining: 59
RateLimit-Reset: 1

The RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers carry the same reading for clients that only understand those. They describe one policy only: the one with the least remaining (on a tie, the later reset). Prefer the structured pair.

A refusal carries the same headers, with r=0 for the policy that refused it:

HTTP/1.1 429 Too Many Requests
Retry-After: 4
RateLimit-Policy: "submit";q=60;w=60
RateLimit: "submit";r=0;t=4

The daily item appears only when your plan has a daily quota. Its r is advisory: it is read just after your job was admitted, so a submission made at the same moment can make it lag by one. The limit itself is enforced exactly.

How the per-minute limits refill

The submit and upload policies are either a fixed window, which refills all at once when the minute ends, or a token bucket (GCRA), where each call spends one of q tokens and tokens come back steadily, one every w / q seconds. Either way t is the honest wait: the rest of the minute for a fixed window, the next token for the bucket.

What is not advertised

Two limits are enforced without headers: how many of your jobs may wait in the queue, and how many may run at once. Both change as jobs start and finish, so a number in one response would be stale by the next. A full queue answers TENANT_QUEUE_LIMIT_EXCEEDED with Retry-After; a concurrency limit only delays when a queued job starts and never refuses a submission.

In the SDKs

Both SDKs parse the headers, so you do not have to. Where the headers are off, the readings below are empty.

import { MorphiaOne, parseRateLimit } from '@morphiaone/sdk';
const client = new MorphiaOne({ apiKey, baseUrl: 'https://api.morphiaone.ai' });
await client.skin.analyze({ assetId, concerns: ['acne'] });
console.log(client.lastRateLimit); // [{ policy, limit, remaining, reset, window }, ...]

last_rate_limit / lastRateLimit holds what the last submission or upload advertised; polling does not clear it. An error carries its own as error.rate_limit / error.rateLimit. When a RATE_LIMITED refusal has no Retry-After, the SDKs wait the refusing policy’s t before their retry. They never retry QUOTA_EXCEEDED. See Retries and backoff.