# Ensembl REST API Error Handling

The single most common Ensembl REST API error is HTTP 429, "Too Many Requests." Your script is sending traffic faster than the service allows for your account or IP address. The first thing to check is the response status code and the `Retry-After` header that Ensembl returns with a 429. If that header is present, it tells you exactly how long to wait before the next attempt. Most scripts fail not because the request itself is wrong, but because the code never reads that header and instead hammers the server in a tight loop until every request fails.

This guide walks through the full set of Ensembl REST API error handling problems in order of frequency. You get a diagnostic sequence you can follow in under a minute, a symptom table covering every common failure, the mechanism behind each error, working Python and curl examples, and prevention practices that keep long-running pipelines alive. All status code semantics follow the IETF HTTP specifications, and all service-specific behavior follows the Ensembl REST API documentation published by EMBL-EBI. Because Ensembl adjusts limits and endpoints between releases, confirm current values on the official documentation pages before you hard-code them.

## How the Ensembl REST API works, and why requests fail

The Ensembl REST API is a web service at `https://rest.ensembl.org` that exposes the same genome annotations, sequences, variant effects, and cross-references you see in the [Ensembl genome browser](/blog/guides/ensembl-genome-browser). A typical request fetches the cDNA sequence of a gene:

```bash
curl -s -H "Content-Type: application/json" "https://rest.ensembl.org/sequence/id/ENSG00000139618?content-type=application/json&type=cdna"
```

Three design facts explain most of the errors you will ever hit.

First, Ensembl negotiates output format through the `Content-Type` header or a `content-type` query parameter. You ask for `application/json`, `text/xml`, or `text/x-fasta` on the request, not through a separate `Accept` header the way many other APIs do. A missing or misspelled format request is the classic cause of an HTTP 406, "Not Acceptable."

Second, the service is shared infrastructure operated by EMBL-EBI. Anonymous clients and registered clients get different rate limits, and the published limits are enforced per connection source. When you exceed them, the server answers with 429 instead of quietly slowing you down. According to Ensembl's documentation, anonymous users face a much smaller per-minute allowance than registered users, who receive a far larger hourly allowance after signing up through the Ensembl site. Check the official rate limiting page for the exact current numbers, since they have changed over time.

Third, the API is versionless from your point of view but release-aware underneath. `rest.ensembl.org` always reflects the current release and its default assembly. Older assemblies, such as GRCh37 for human, live on archive endpoints such as `grch37.rest.ensembl.org`. Requests that assume the wrong assembly produce 404s and silent data mismatches rather than obvious failures.

Every error response carries a JSON body with an `error` key describing the problem. Reading that body is the fastest way to understand a failure, and it is the step most people skip.

## The 60-second diagnostic sequence

![Decision path splitting Ensembl API errors into 429 and 5xx responses that get retried versus 4xx requests that must be fixed.](/article-images/illustrations/ensembl-rest-api-error-handling-status-triage.webp)

Run these checks in order. They resolve or classify nearly every Ensembl REST API error.

1. Re-run the failing request with `curl -i` (or `curl -v`) so you can see the full response, including status code and headers.
2. Read the status code. Codes starting with 4 mean your request is the problem. Codes starting with 5 mean the service had a problem.
3. If the code is 429, read the `Retry-After` header and note its value in seconds.
4. Read the JSON error body. It usually names the exact parameter, identifier, or endpoint that failed.
5. Test the health endpoint, `https://rest.ensembl.org/info/ping?content-type=application/json`, to confirm the service itself is up.
6. If the request involves an identifier, check its format. Ensembl gene, transcript, and protein identifiers look like `ENSG`, `ENST`, or `ENSP` followed by 11 digits, for example `ENSG00000139618`. A truncated or mispaced identifier fails predictably.
7. If the request involves a genomic region, shrink the region. Very large windows are a frequent trigger for timeouts and server errors.
8. If your script sends many requests, print the status code of every response, not only the successful ones.

## Troubleshooting table

This table is the core reference. Work from the top, which lists the most common failures.

| Symptom | Likely cause | How to confirm | Fix |
|--|--|--|--|
| HTTP 429, "Too Many Requests," often with a `Retry-After` header | Rate limit exceeded | `curl -i` shows 429 and the header value | Honor `Retry-After`, add exponential backoff, batch with POST, register for higher limits |
| HTTP 406, "Not Acceptable" | Missing or unsupported format request | Response body says the requested content type is not supported | Send `Content-Type: application/json` or add `?content-type=application/json` |
| HTTP 415, "Unsupported Media Type" | Body sent to a POST endpoint without a declared content type | Happens only on POST requests | Set `Content-Type: application/json` and `Accept: application/json` on POST calls |
| HTTP 404 with "not found" in the body | Unknown identifier, wrong species, wrong assembly, or mistyped endpoint | Look the ID up manually in the Ensembl genome browser, or query `/lookup/id` | Correct the ID and species parameter, or switch to the archive endpoint for the assembly you need |
| HTTP 400, "Bad Request" | Malformed parameters, invalid region syntax, or an ID that does not match the species | Error body names the offending parameter | Validate inputs before sending, check region format like `7:140424943-140524943` |
| HTTP 405, "Method Not Allowed" | GET used on an endpoint that requires POST, or the reverse | Compare your HTTP method against the endpoint documentation | Switch to the documented method |
| HTTP 403 | Access blocked, often repeated limit violations or disallowed traffic patterns | Status persists after fixing rate behavior | Slow down substantially, register, and contact the helpdesk if it persists |
| HTTP 500 or connection reset mid-loop | Very large request or transient server-side problem | The same request succeeds when made smaller | Reduce region size, batch into smaller POSTs, retry with backoff |
| HTTP 502 or 503, often around release weeks | Scheduled maintenance or deployment | Ensembl announces releases on its site and status channels | Schedule pipeline runs outside release windows and retry later |
| JSON decode error in your own code | Your parser hit an error body or a proxy page instead of expected data | Print the raw response text and status code | Check the status code before parsing, handle the `error` key explicitly |
| Requests hang indefinitely | No timeout set in your HTTP client | Script freezes on one request | Set explicit connection and response timeouts, as Ensembl's documentation recommends |

## Cause-by-cause guide, from most common to rarest

### HTTP 429, the rate limit you almost certainly hit first

Rate limiting is the dominant cause of Ensembl REST API failure in scripted work. The mechanism is simple. EMBL-EBI operates the service for the entire community, so it caps request rates per client. The 429 status code, defined in the IETF HTTP specification, means the client has sent too many requests in a given window.

The failure pattern looks like this. A script loops over 5,000 gene IDs, sends requests as fast as the network allows, gets 429 on roughly the fiftieth request, and the author sees a wall of tracebacks. The requests were individually valid. The pace was the problem.

How to fix it, in escalating order of effort.

1. Honor `Retry-After`. When Ensembl returns 429, the response includes a `Retry-After` header giving a wait time in seconds. Sleep for that duration, then retry once.
2. Add exponential backoff with jitter for 429 and 5xx responses. Doubling the wait after each failure, with random jitter to keep parallel workers from syncing, is the standard pattern.
3. Throttle proactively. Keep your request rate safely below the published limit rather than racing the ceiling. A small sleep between requests, tuned to your allowance, prevents 429s entirely in most workloads.
4. Batch. Many endpoints accept POST requests with lists of identifiers, which Ensembl documents with a per-request cap on the order of one thousand identifiers. One batched call replaces up to a thousand individual GETs and reduces your rate consumption dramatically.
5. Register. Registered users receive a substantially higher allowance than anonymous users. Follow the registration and authentication instructions in the Ensembl REST documentation and attach your token in the `Authorization` header of each request.
6. Cache. Never fetch the same lookup twice in one pipeline. Store responses locally, keyed by endpoint and parameters, and check the cache before every request.

A credential-hygiene note applies here. Registered access uses a personal token. Treat it like a password. Do not commit it to public repositories, do not paste it into shared notebooks, and load it from an environment variable or a secret store at runtime.

### HTTP 406 and 415, content negotiation mistakes

The second most common failure is a format problem. Ensembl decides what to return based on the content type you request. If you send a request with no `Content-Type` header and no `content-type` parameter, or you ask for a format the endpoint does not produce, the server returns 406.

The mechanism trips people up because the request is otherwise perfect. This returns 406:

```bash
curl -s "https://rest.ensembl.org/sequence/id/ENSG00000139618"
```

This works:

```bash
curl -s -H "Content-Type: application/json" "https://rest.ensembl.org/sequence/id/ENSG00000139618"
```

The same rule applies in Python:

```python
import requests

r = requests.get(
    "https://rest.ensembl.org/sequence/id/ENSG00000139618",
    headers={"Content-Type": "application/json"},
    timeout=(10, 60)
)
print(r.status_code)
```

For POST endpoints, which take a JSON body, set both `Content-Type: application/json` and `Accept: application/json`. Ensembl's documentation lists the formats each endpoint family supports, and FASTA output is only available on the endpoints that produce sequences.

### HTTP 404, wrong identifier, wrong species, or wrong assembly

A 404 from Ensembl REST has three distinct flavors, and the error body usually tells you which one you have.

An unknown endpoint means the URL path itself is wrong. Confirm the path against the endpoint documentation. A simple sanity check is `/info/ping`, which should return a JSON success object whenever the service is up.

An unknown identifier means the ID does not exist in the species you specified. Ensembl identifiers are species-neutral in format, so `ENSG00000139618` is human, but a mouse gene passed with `species=human` fails. Two practical checks help. Query the `/lookup/id` endpoint with the ID and see what comes back, or paste the ID into the Ensembl genome browser search box, which resolves identifiers across species and assemblies.

A wrong assembly produces the most misleading failures. `rest.ensembl.org` serves the current release. If your coordinates or your workflow are anchored to GRCh37, the current human assembly GRCh38 will disagree with your data, and lookups may 404 or return coordinates that silently mismatch. For GRCh37 work, use the dedicated archive endpoint `grch37.rest.ensembl.org`, which mirrors the REST API for that assembly. The same principle holds for other archived assemblies listed on the Ensembl archive page. Always record the assembly and release alongside every result your pipeline produces.

### HTTP 400, malformed requests

A 400 means the server understood the endpoint but rejected the parameters. Typical triggers include a region string in the wrong format, a coordinate outside the assembly bounds, conflicting query parameters, or an identifier embedded in the URL where the endpoint expects none. The JSON error body names the problem, so read it before changing anything.

Region parameters use the form chromosome:start-end, for example `7:140424943-140524943`. A stray space, a comma in the coordinates, or a chromosome name the species does not use, such as `chr7` where Ensembl expects `7`, produces a 400. Ensembl's coordinate convention omits the `chr` prefix, which is the single most common formatting error in code ported from UCSC-based scripts.

### HTTP 405, wrong HTTP method

Some endpoints only accept POST because they take large payloads. Sending a GET to a POST-only endpoint returns 405. This usually happens when someone converts a working GET request into a batched version by appending IDs to the URL instead of moving them into a JSON body. The fix is to switch the method and move the payload.

### HTTP 5xx, server-side failures

Codes starting with 5 mean the problem was on the service side. Two patterns matter in practice.

Transient 500s and connection resets tend to correlate with request size. The region endpoint over a whole chromosome, or a variant effect calculation over thousands of variants, can exceed practical response limits. The server may abort the connection rather than return a clean error. Reducing the region or splitting the identifier list usually converts a failing request into several succeeding ones.

Sustained 502s and 503s cluster around Ensembl release cycles, when new assemblies and annotations are deployed. These are not errors you can code your way around. Build your pipeline to retry with backoff, and schedule large runs outside known release windows. Ensembl announces release schedules on its website.

### Network-level failures, mirrors, and archives

Below the HTTP layer, a few environment problems mimic API errors.

Corporate proxies and firewalls can rewrite or block HTTPS traffic, returning HTML error pages that break JSON parsers. If `curl` succeeds from your terminal but Python fails on the same URL, inspect the raw response text to see what actually came back, and check your proxy environment variables.

Expired TLS certificates on a local man-in-the-middle proxy produce certificate verification errors in Python that look nothing like API errors. Fix the trust store rather than disabling verification.

Non-vertebrate work introduces a hosting question. Much of the non-vertebrate annotation is served through Ensembl Genomes, and the species or division you request may live on a different service than the vertebrate database. If a species lookup 404s even though the species obviously exists in Ensembl, check whether Ensembl Genomes hosts it and consult the relevant documentation for the correct service.

## A reference retry client

![Worked calculation of the exponential backoff wait used in the reference retry client, doubling from attempt zero.](/article-images/illustrations/ensembl-rest-api-error-handling-backoff-wait.webp)

The following Python function handles the four behaviors that prevent most failures. It sets explicit timeouts, honors `Retry-After`, applies exponential backoff with jitter, and raises a clear error for client mistakes it should not retry.

```python
import time
import random
import requests

BASE_URL = "https://rest.ensembl.org"
HEADERS = {"Content-Type": "application/json"}

def ensembl_get(path, params=None, max_retries=6):
    """GET from the Ensembl REST API with backoff on 429 and 5xx."""
    url = BASE_URL + path
    for attempt in range(max_retries):
        try:
            r = requests.get(
                url,
                params=params,
                headers=HEADERS,
                timeout=(10, 60)
            )
        except requests.RequestException as exc:
            wait = 2 ** attempt + random.random()
            time.sleep(wait)
            continue

        if r.status_code == 200:
            return r

        if r.status_code == 429:
            retry_after = r.headers.get("Retry-After")
            wait = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(wait + random.random())
            continue

        if r.status_code in (500, 502, 503, 504):
            time.sleep(2 ** attempt + random.random())
            continue

        # 4xx other than 429 means the request itself is wrong.
        r.raise_for_status()

    raise RuntimeError(f"Ensembl request failed after {max_retries} attempts: {url}")
```

Three details in this code deserve comment. The timeout tuple sets ten seconds to connect and sixty seconds for the response, which prevents the infinite hangs that freeze pipelines. The `raise_for_status` path deliberately refuses to retry 400-class errors, because retrying a malformed request just wastes your allowance. The jitter added to every wait keeps multiple parallel workers from retrying in lockstep and reproducing the same 429 storm together.

For production pipelines, the `urllib3` `Retry` adapter attached to a `requests.Session` gives the same behavior with less code, and it respects `Retry-After` by default. The R ecosystem offers the equivalent through `httr2`, whose `req_retry()` helper handles backoff and `Retry-After` transparently. The concepts, not the library, are what matter.

## Batching with POST to cut error rates

Batching is the highest-leverage fix available, because it attacks rate limiting at its root. Where a GET endpoint accepts one identifier, the corresponding POST endpoint accepts a JSON array of identifiers, subject to the per-request cap documented by Ensembl.

A batched lookup looks like this:

```python
import requests

ids = ["ENSG00000139618", "ENSG00000141510", "ENSG00000012048"]

r = requests.post(
    "https://rest.ensembl.org/lookup/id",
    headers={"Content-Type": "application/json", "Accept": "application/json"},
    json={"ids": ids, "species": "human"},
    timeout=(10, 120)
)
print(r.status_code)
print(r.json())
```

Batching changes your error profile in two ways. First, a workflow that needed 3,000 requests can shrink to a handful, dropping you far below the rate limit. Second, a failed batch tells you less about which single identifier was at fault, so keep the batch size moderate, log request and response identifiers, and re-query failures individually to isolate them.

## Using Ensembl itself alongside the API

Error handling makes more sense once you know what the API is doing relative to the browser. A short orientation on how to use Ensembl helps here, and it doubles as a debugging tool.

The Ensembl genome browser at ensembl.org is the point-and-click interface to the same underlying databases the REST API serves. When you search for a gene, the browser shows a location view with tracks for transcripts, regulatory features, variants, and [comparative genomics](/blog/guides/comparative-genomics). Clicking through to a gene page exposes stable identifiers, coordinates on the current assembly, transcript tables, and export options for sequences and whole-region data. A basic [genome browser tutorial](/blog/guides/genome-browsers-for-researchers-a-guide-to-inspecting-genomic-evidence) pattern is worth memorizing: search by gene symbol, confirm the species and assembly in the page header, open the region in the location view, and use the export menu for bulk data. Every one of those steps has a REST equivalent, which is why the browser is your best error-checking tool.

When the API returns a 404 or a coordinate you did not expect, paste the identifier into the browser search box. If the browser finds it, the API request is wrong, usually the species or assembly parameter. If the browser cannot find it either, the identifier itself is stale or mistyped. Stable identifiers can be retired and replaced across releases, so an ID that worked last year may genuinely no longer exist. Ensembl provides an ID history service to map retired identifiers to their current equivalents.

The browser is also where you confirm assembly before writing any API call. The assembly name appears in the browser interface on every location page. Recording that assembly with your results protects your pipeline from the silent mismatch failure described earlier.

## Common Mistakes and Limitations

These are the mistakes that show up repeatedly in real pipelines.

- Ignoring `Retry-After`. The header exists precisely to tell you when to come back. Retrying immediately after a 429 deepens the violation and can escalate to 403.
- Retrying 4xx errors. A 400 or 404 will fail identically forever. Only 429, 408, and 5xx classes deserve retries, and even then with a cap.
- No timeouts. A pipeline without explicit connection and response timeouts can hang for hours on one stalled socket. Set both, always.
- Silent parsing. Checking only whether the response body parses as JSON hides 429 and 500 responses that happen to return parseable error bodies. Check the status code first, every time.
- Hard-coding the assembly. Code that assumes the current human assembly breaks whenever you point it at GRCh37 data. Pass assembly and species explicitly and record them with the output.
- Unbounded concurrency. Spawning hundreds of threads multiplies your effective request rate and guarantees 429s. Keep parallel workers modest and share one throttle across them.
- No caching. Refetching the same `ENSG` lookup in three different scripts triples your load for zero benefit. Cache responses on disk keyed by the full request.
- Rebuilding on every release without testing. Endpoints, limits, and assemblies evolve. A pipeline that worked on the previous release should be smoke-tested against a handful of known IDs after each release.

The standing limitations are these. Ensembl adjusts rate limits, endpoints, and hosting arrangements between releases, so treat every numeric limit in this article as a value to verify against the current official documentation rather than a permanent constant. Error bodies are informative but not guaranteed to be stable strings, so branch on status codes rather than on error text. And any result that feeds a publication or a clinical interpretation should be traced back to the primary annotation records and release notes, not taken from a single API response alone.

## Prevention practices

A few habits eliminate nearly all Ensembl REST API errors before they happen.

- Register once, store the token in an environment variable, and use the higher allowance it buys you.
- Throttle to a fixed, conservative requests-per-second rate that sits comfortably under your allowance, and enforce it in one place shared by all workers.
- Batch with POST wherever an endpoint allows it.
- Cache aggressively. Ensembl annotation data changes only at releases, so a cache valid within a release is correct by construction.
- Retry with exponential backoff, jitter, and a maximum attempt count, and only on 429 and 5xx.
- Log the status code, the endpoint, and the parameters of every request, so failures come with their own diagnosis.
- Set timeouts on every request, and treat a hung request as a failure to retry, not a reason to wait forever.
- Schedule bulk runs outside release windows, and smoke-test after each release with a small set of reference identifiers.

## Frequently Asked Questions

### What does a 429 error from the Ensembl REST API mean?

It means you exceeded the service's rate limit for your IP address or account. Read the `Retry-After` header, wait that long, and add exponential backoff and request throttling to your script so the limit is not hit again.

### Why do I keep getting HTTP 406 errors?

You are not requesting a supported output format. Add `Content-Type: application/json` to your headers, or append `?content-type=application/json` to the URL, and confirm the format is one the endpoint actually produces.

### Can I raise my Ensembl REST API rate limit?

Yes. Register through the Ensembl website for registered-user access, which carries a substantially higher allowance than anonymous access, and follow the authentication instructions in the official documentation.

### Why does my script work for one request but fail inside a loop?

Each individual request is fine, but the loop's aggregate pace exceeds the rate limit. Add a deliberate delay between requests, batch identifiers with POST, and cache responses you have already fetched.

### What is the difference between GET and POST on the Ensembl REST API?

GET fetches one resource at a time with parameters in the URL. POST submits a JSON array of identifiers in the request body and returns results for many identifiers in one call, which is far cheaper against your rate limit. Check the per-request identifier cap in the documentation.

### How do I query an older assembly such as GRCh37?

Use the archive endpoint, for example `grch37.rest.ensembl.org` for human GRCh37, instead of the main `rest.ensembl.org` host. The main host always reflects the current release and assembly.

### How can I tell whether Ensembl itself is down rather than my code?

Call the health endpoint at `https://rest.ensembl.org/info/ping` with a browser or curl. If it also fails with 5xx errors from a clean network, the problem is on the service side, and you should check Ensembl's announcements and retry later.

### What should I do if an error persists after fixing my code?

Slow your request rate to a minimal level, confirm your request matches the documented endpoint and parameters exactly, and contact the Ensembl helpdesk with the full request URL, the status code, and the error body. Persistent 403 responses in particular usually need their intervention.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What does a 429 error from the Ensembl REST API mean?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It means you exceeded the service's rate limit for your IP address or account. Read the Retry-After header, wait that long, and add exponential backoff and request throttling to your script so the limit is not hit again."
      }
    },
    {
      "@type": "Question",
      "name": "Why do I keep getting HTTP 406 errors?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "You are not requesting a supported output format. Add Content-Type: application/json to your headers, or append ?content-type=application/json to the URL, and confirm the format is one the endpoint actually produces."
      }
    },
    {
      "@type": "Question",
      "name": "Can I raise my Ensembl REST API rate limit?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Register through the Ensembl website for registered-user access, which carries a substantially higher allowance than anonymous access, and follow the authentication instructions in the official documentation."
      }
    },
    {
      "@type": "Question",
      "name": "Why does my script work for one request but fail inside a loop?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Each individual request is fine, but the loop's aggregate pace exceeds the rate limit. Add a deliberate delay between requests, batch identifiers with POST, and cache responses you have already fetched."
      }
    },
    {
      "@type": "Question",
      "name": "What is the difference between GET and POST on the Ensembl REST API?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "GET fetches one resource at a time with parameters in the URL. POST submits a JSON array of identifiers in the request body and returns results for many identifiers in one call, which is far cheaper against your rate limit. Check the per-request identifier cap in the documentation."
      }
    },
    {
      "@type": "Question",
      "name": "How do I query an older assembly such as GRCh37?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use the archive endpoint, for example grch37.rest.ensembl.org for human GRCh37, instead of the main rest.ensembl.org host. The main host always reflects the current release and assembly."
      }
    },
    {
      "@type": "Question",
      "name": "How can I tell whether Ensembl itself is down rather than my code?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Call the health endpoint at https://rest.ensembl.org/info/ping with a browser or curl. If it also fails with 5xx errors from a clean network, the problem is on the service side, and you should check Ensembl's announcements and retry later."
      }
    },
    {
      "@type": "Question",
      "name": "What should I do if an error persists after fixing my code?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Slow your request rate to a minimal level, confirm your request matches the documented endpoint and parameters exactly, and contact the Ensembl helpdesk with the full request URL, the status code, and the error body. Persistent 403 responses in particular usually need their intervention."
      }
    }
  ]
}
</script>