Skip to main content
.md

Limits

This page is the single owner of the API's numeric caps and rate quotas. For per-endpoint request/response schemas and the error.code list, see ReDoc; for how to handle each error, see Errors.

Input length per task

Each task carries its own minimum, published as minLength on that task's request schema and enforced at request validation — before any model is loaded. The maximum is 500,000 bp everywhere. Bounds are minLength/maxLength on each task's request schema in /v1/openapi.json; per-model context windows (context_window_bp) are served by GET /v1/tasks/{task}/models.

A task's floor is the strictest its models need — it is not per-model, and every listed model stays selectable. Sequences must contain only A/C/G/T/N (case-insensitive); oversized, undersized, or out-of-alphabet inputs are rejected with 422 validation_failed before any GPU work. Request body cap: 16 MiB.

All lengths are counted after ASCII whitespace is stripped, so a line-wrapped FASTA sequence body can be pasted verbatim (a > header line cannot — it fails the alphabet check).

That cuts both ways, and it is the one place the published schema and the server disagree in a way worth knowing: minLength/maxLength are checked by a generated client against the raw string you hand it, and by the server against the stripped one. 9,000 bases wrapped over 200 lines is 9,200 raw characters — it satisfies expression's published 9,198 minimum as a string and is still rejected once stripped. Count bases, not characters.

Since contract revision 5 the alphabet is machine-readable too: every sequence field publishes pattern as ^\s*(?:[ACGNTacgnt]\s*)+$, so a generated client enforces it without you retyping the rule. It admits lowercase and inline whitespace deliberately — that is what the server accepts, and a stricter pattern would make your client refuse sequence the API scores.

The floor is admission control, not regime

Above the floor is not the same as in regime

A sequence at or above the task floor but shorter than the selected model's context_window_bp is accepted and scored — against a window padded out to the context window. Nothing in the response flags this. Enhancer is the sharpest case: the bound is 50 bp, but g0-deepstarr scores 249 bp windows, so a 60 bp submission is ~75% padding.

Compare your length against context_window_bp — served per model by GET /v1/tasks/{task}/models as part of bio_spec — to know whether the model saw real sequence or padding. Longer-than-context input is never a problem: the scanner steps one prediction window at a time and pads only the final partial window.

bio_spec also carries request_max_bp (the enforced ceiling — 500,000 for every model, g0-expression included) and trained_window_bp (a fixed receptive field where one exists: 9,198 for g0-expression, null for sliding-window models). Use request_max_bp as the cap. Older responses may also carry max_seq_length_bp; it is ambiguous — the request cap for every model except g0-expression, where it is instead the trained window — so do not read it, and do not depend on it being present.

Which status you get

SituationResponse
sequence below the task floor or above 500,000 bp422 validation_failed (not 413), loc ["body","sequence"]
Unknown key inside options422 validation_failed, loc ["body","options","<key>"], type extra_forbidden
Raw request body above 16 MiB413 payload_too_large, refused before parsing
Composite submitted synchronously above 50,000 bp, or annotation above 200,000 bp413 sync_too_large (see Hard sync cap)
Splice response would carry more than 20,000 sites422 validation_failed (see Response size)
Unknown {task} segment404 not_found
Unsupported ?format= value415 unsupported_format — never a silent fallback to JSON

Expression: the 9,198 bp window and tss_index

9,198 bp is a floor, not a fixed size: the endpoint accepts up to 500,000 bp and windows server-side. What is fixed is what the model scores — exactly one 9,198 bp TSS-centered window (±4,599 bp), cut out of whatever you submit. A sequence shorter than that window is rejected with 422 validation_failed; there is no flag, header, or query parameter that restores padding or truncation. (Client-side gates written as len(seq) != 9198 are wrong — the check is len(seq) >= 9198 on the whitespace-stripped string.)

You may submit anywhere from one exact window up to a 500,000 bp locus and let the server cut the window for you:

Submitted sequencetss_index
exactly 9,198 bpoptional — defaults to 4599, the only legal value at that length
9,199–500,000 bprequired

tss_index is the 0-based offset of the transcription start site into the whitespace-stripped sequence — not a file character offset, and not a genomic coordinate. Compute it after removing newlines, or a line-wrapped FASTA will be off by its newline count. It must satisfy 4599 <= tss_index <= len(sequence) - 4599; violations are rejected with 422, never clamped or padded. The server then scores the half-open slice sequence[tss_index - 4599 : tss_index + 4599].

Windowing happens before tokenization, so a 500,000 bp submission costs one string slice — there is no forced-async threshold on this endpoint.

warning

The legal tss_index range is wide, so an index that is merely wrong (computed against un-stripped FASTA characters, or against a locus start rather than the submitted slice) does not error — it returns a confident 200 for the wrong window. Assert on the echoed meta.task_specific_counts.scored_window (also data.input.scored_window) to confirm the server scored the window you meant.

The endpoint does not discover the TSS for you and does not reverse-complement: submit the gene's coding (sense) strand. If you have raw genomic input where the TSS isn't known, use the composite find-genes-and-predict-expression workflow instead — it finds the genes and centres each window itself, so it takes no tss_index and its floor is annotation's 1,000 bp rather than 9,198.

Response size: the splice site cap

Input bounds admit a request; one cap bounds what a request may emit. POST /v1/tasks/splice/predict refuses any request whose response would carry more than 20,000 splice sites, with 422 validation_failed:

{
"error": {
"code": "validation_failed",
"message": "splice prediction exceeded the 20000 record response cap at threshold 0.0 on a 60000 bp sequence. Raise options.threshold and retry — a threshold at or below 0 marks nearly every token as a site regardless of the biology.",
"request_id": "a375d0cf-4ffd-4317-b3c5-fcbf4292c98e",
"details": {
"task": "splice",
"record_count": 20002,
"maximum_records": 20000,
"sequence_length": 60000,
"threshold": 0.0
}
}
}

details carries record_count, maximum_records, sequence_length and threshold, so you can compute the retry without parsing the message. Note this is the one validation_failed whose details is not the {errors: [...]} array — see Errors.

This is the only response-size cap in the API, and splice is the only task with one. It exists because splice is token-level: at a low options.threshold the site count tracks sequence length rather than biology, and every site is serialised three times (in sites, in tracks, and in the BED/GFF3 strings under formats). At threshold: 0 a 500,000 bp input would emit roughly 176,000 sites and ~64 MB.

Three things worth knowing about how it fires:

  • Only a low threshold reaches it. At the 0.5 default a 500,000 bp input returns a few hundred sites — the cap sits ~66x above any plausible real result. Normal traffic never sees it.
  • It is not admission control. The check runs while sites accumulate during inference, not before the model loads, so it costs a few seconds rather than failing instantly.
  • The request is rejected, not truncated. A silently short site list would read as a complete one.

The fix is always a higher options.threshold, never a shorter sequence.

Don't use threshold: 0 — use 1e-3

If you are lowering the threshold to "get everything", set it to 1e-3, not 0. 0 marks nearly every token a site regardless of the biology, and it is what the cap exists to reject.

1e-3 is not a compromise. In our SGE splice benchmark, scoring at 1e-3 instead of 0 moved no score by more than 4.6e-4 — entirely inside the noise band you would discard anyway — and ran about 4x faster, because the bottleneck is serialising the response, not GPU inference. On a 60,000 bp input, threshold: 0 is a 422 while 1e-3 and 0.5 both return in ~2 s.

Latency

Rough sync latency at the recommended input size, on a warm model.

TaskSync latency at recommended sizeWhen to go async
Promoter / enhancer / chromatin0.3–10 sinputs above 100,000 bp
Splice0.3–10 sinputs above 250,000 bp
Annotation1–60 sinputs above 30,000 bp
Expression (see below)0.5–3 sn/a — sync is always safe

Cold start

If a task's model isn't already loaded into GPU memory, the first request pays a model-load cost. The response carries meta.cold_start: true and meta.model_load_time_ms. Cold start adds 5–15 s for the smaller models and 30–90 s for expression and annotation. Subsequent calls are warm.

Sync delivery: timeout and guidance

Sync delivery (the default, no Prefer header) is best-effort within the upstream HTTP read timeout of 300 seconds. A request that takes longer than that is terminated by the edge proxy and surfaces to your client as a connection reset or 504 gateway_timeout. The body in this case is the proxy's, not the unified {error: {...}} envelope; this is the only place where that happens. Pick async whenever you expect a request to push past ~60 s.

Hard sync cap

Two operations enforce a hard sync cap, and both return 413 sync_too_large to a synchronous request above their threshold:

OperationSync cap
POST /v1/workflows/find-genes-and-predict-expression (the composite)50,000 bp
POST /v1/tasks/annotation/predict200,000 bp

Resend the identical body with Prefer: respond-async (as the MCP server's find_genes_and_predict_expression and find_genes tools always do) to lift the cap. The other five predict endpoints — promoter, splice, enhancer, chromatin, expression — have no sync cap at all: each accepts sync up to its per-task max (see "Input length per task" above), and none of them emits sync_too_large.

The cap governs JSON delivery. Async jobs return JSON, so a request that asks for ?format=bed or ?format=gff3 is not capped: annotation still serves those synchronously above 200,000 bp, which means a large text-format request can run long enough to hit the 300 s proxy timeout described above. Size those calls yourself.

Both thresholds are published structurally, not just here in prose: each is x-sync-limit-bp on its own operation in /v1/openapi.json, so a client can read them instead of hardcoding them — and the absence of that extension on the other five predict operations is the contract saying they are uncapped.

Use Prefer: respond-async when your input exceeds the threshold below. These are calibrated against typical inference times. Sync still works under them, but bursty traffic plus GPU contention can push individual requests past the 300 s proxy window without warning.

TaskRecommended async above
Promoter100,000 bp
Splice250,000 bp
Enhancer100,000 bp
Chromatin100,000 bp
Annotation30,000 bp — advisory; above 200,000 bp it becomes enforced, sync JSON delivery is 413 sync_too_large
Expressionn/a — sync is always safe regardless of submission size (see above)
Find genes + predict expression50,000 bp — and this one is enforced, not advisory: above it, sync delivery is 413 sync_too_large

If sync is critical for your workload and these guidelines push more traffic to async than you'd like, contact us; we can profile your distribution and tune the proxy timeout for your account.

Per-key quotas

Two limiters run per key — concurrent in-flight requests and requests per minute — plus a fixed cap at the edge. Caps are issued per key; your account owner tells you the values for yours. You can also read your current limit off the RateLimit-Limit response header (your rpm ÷ 6) on any authenticated response — see below.

SettingEnforced?
Concurrent in-flight requestsYes. Exceeding the cap returns 429 too_many_requests with Retry-After: 1.
Per-minute request rateYes. Token bucket; capacity = rate / 6 (10-second burst). An empty bucket returns 429 too_many_requests with Retry-After set to approximately the seconds until the next token.
Edge per-IP cap10 r/s burst 20 on api.*. Yes, at the edge; returns 429 too_many_requests with the unified {error: {...}} envelope (see errors).

The ÷ 6 burst divisor above is also published structurally as x-rate-limit-burst-divisor at the root of /v1/openapi.json — read it from there rather than hardcoding 6.

RateLimit-* headers (every authenticated response)

The application emits the IETF httpapi-ratelimit-headers draft set on every authenticated 2xx and on every 429 from this service, so you can pace from header state without inferring rate from 429s:

HeaderMeaning
RateLimit-LimitToken-bucket capacity (= rate_per_minute / 6).
RateLimit-RemainingTokens left after this request, integer.
RateLimit-ResetSeconds until the bucket refills to full.
RateLimit-Policy<capacity>;w=60. The w=60 is the draft's window syntax, not a statement that capacity is your per-minute allowance — see below.
Retry-AfterOn 429 only, seconds until at least one token is available.

Pacing guidance

  • RateLimit-Limit is a burst capacity, not your per-minute budget. It is your tier's rate_per_minute divided by 6 (a 10-second burst), so a 300 rpm key advertises RateLimit-Limit: 50. Sizing a minute's work from the header alone leaves you pacing 6x slower than your tier allows. Pace sustained work against your issued rpm; use RateLimit-Remaining to avoid emptying the bucket in a burst.
  • Pace at ~80% of RateLimit-Limit for burst headroom, and serialize a small worker pool against your concurrency cap rather than firing N parallel requests at it.
  • The token bucket and concurrency semaphore are independent: a 429 can come from either. Both carry the same RateLimit-* spine, but only the rate-bucket 429 has a Retry-After calibrated against refill time; the concurrency 429 uses Retry-After: 1.

Async result store

Async job results are retained for 24 h from last activity, then expire. A fetch after the TTL (or after a service restart) returns 410 job_expired, so retrieve results promptly and re-submit if a job has expired.


Genomic variant interpretation

This workflow takes a VCF rather than a sequence, so the bp caps above do not apply to it. Its own bounds:

BoundValue
Input VCF2 GiB maximum, and it must not be empty
InputOne VCF object, named directly; the key must end .vcf, .vcf.gz or .vcf.bgz
options.window1–100,000 bp, default 5,000
options.tissues1–16 entries, default ["heart", "liver", "brain"]
options.genesUp to 1,000 HGNC symbols or Ensembl gene ids; omit to score every nearby gene
client_ref128 characters
DeliveryAsynchronous only — Prefer: respond-async is required
Result retention24 hours, then 410. The annotated file in your bucket is not on that clock

Uncompressed, bgzip-compressed and plain gzip-compressed VCFs are all accepted. Full contract: Tasks.

Back to the workflow: REST API guide · Tasks.