Skip to content
x.md
Esc
↑↓navigate↵open⌘Jpreview
On this page

Errors, limits, and caching

What a failure looks like, how much room a caller has left, and when a response came from cache.

Errors

Every failure answers with an RFC 9457 problem document, served as application/problem+json. It carries a stable machine code, a human detail, and a one-sentence resolution.

{
  "type": "https://fast.mdfromx.com/docs/reliability#invalid-handle",
  "title": "Invalid X handle",
  "status": 400,
  "detail": "Invalid X handle",
  "instance": "https://fast.mdfromx.com/api/v1/profiles/not%20a%20handle",
  "code": "invalid_handle",
  "resolution": "Handles are 1-15 characters of letters, digits, or underscores, with no leading @.",
  "documentation_url": "https://fast.mdfromx.com/docs/reliability#errors",
  "error": "Invalid X handle"
}

Branch on status for the class of failure and on code for the specific one. error is an alias of detail, kept for clients written against the older { error, code } shape. Each problem’s type is a stable URI that points back into this documentation.

Status Typical cause What to do
400 Missing query, invalid handle, unsupported parameter value Fix the request. Retrying it unchanged fails identically.
401 An API key was offered and rejected Drop the header to call anonymously. The public API needs no credentials.
404 Confirmed missing or inaccessible content, or an unknown path Follow the links in the body. Check the public source URL on x.com. An ambiguous provider miss uses 503.
405 A write method on a read-only route Use GET, HEAD, or OPTIONS; read Allow.
406 An Accept header that excludes every representation Ask for text/markdown, application/json, or text/html.
429 A quota is exhausted Sleep for Retry-After seconds, then retry once.
502, 503 Provider failure, ambiguous profile miss, or live search has no healthy session Honor Retry-After when present; retry with backoff and jitter.
500 A bug in x.md Retry with backoff; if it persists, open an issue.

The full error catalogue lists every machine code with its meaning and its recovery step.

Error types

A problem’s type URI lands on its code’s row below. Each code links to its section of the error catalogue, which gives its meaning and recovery step.

Code Status Title
missing_url 400 Missing url parameter
invalid_url 400 Invalid X status URL
unsupported_host 400 Unsupported host
invalid_path 400 Not a status permalink
invalid_params 400 Invalid handle or status id
invalid_option 400 Invalid query option
invalid_handle 400 Invalid X handle
invalid_resource 400 Unsupported browse resource
invalid_format 400 Unsupported format
invalid_thread 400 Invalid thread parameter
invalid_userinfo 400 Invalid userinfo parameter
invalid_context 400 Invalid context parameter
invalid_replies 400 Invalid replies parameter
invalid_mode 400 Invalid mode parameter
missing_query 400 Missing search query
invalid_body 400 Invalid JSON body
invalid_key 401 Invalid or disabled API key
unauthorized 401 Unauthorized
not_found 404 Not found
route_not_found 404 API route not found
method_not_allowed 405 Method not allowed
not_acceptable 406 No acceptable representation
rate_limited 429 Rate limit exceeded
internal_error 500 Unexpected error
upstream_error 502 Upstream provider error
partial_upstream_failure 502 Partial upstream failure
import_busy 503 Fresh import capacity is busy
upstream_unavailable 503 Upstream profile unavailable
upstream_rate_limited 503 Upstream provider is rate limiting x.md
search_unavailable 503 Search temporarily unavailable
admin_unconfigured 503 Admin is not configured

Bulk NDJSON imports end with {"meta":…} after all post lines. If one timeline page remains unavailable after retries, the import can finish with meta.warnings: ["page_missing"], truncated: true, and floor_reached: false; retry the range to fill the gap. If it cannot continue, the last line is an RFC 9457 error. After posts have streamed its code is partial_upstream_failure and streamed_posts gives their count; earlier failures can use codes such as upstream_unavailable or internal_error. An HTTP 200 on a stream does not by itself mean the import completed.

Rate limits

Limits exist so the service stays responsive, not to meter usage: it is free and there is nothing to buy. Every response says how much room is left, so a client can pace itself instead of discovering the limit by hitting it.

Policy Quota Window Keyed by Applies to
api-ip 600 60s client IP Every public API route.
search-ip 5 60s client IP Live search lookups by anonymous callers. Charged only when a search misses the cache and reaches a provider.
search-key 30 60s API key The same burst gate for a key holder.
account-ip 10 900s client IP Account-backed feeds (Photos, Videos, Users, and the Latest/Top live fallback), drawn from a shared public pool.
account-key the key’s own 900s API key A key holder’s account-backed allowance, instead of the public one.

Cache hits are free: a response served from the application cache never reaches a counter. An uncached search’s own response already counts that search, so r=0 on a success means the next uncached search will be refused. Only the policies that apply to the route you called are advertised, so a profile read reports api-ip alone and a search reports three.

Response headers

Two forms go out together on every measured response — success, error, and cache miss alike.

RateLimit-Policy and RateLimit are the IETF structured fields: a comma-separated list of named policies, q = quota, w = window seconds, r = units remaining, t = seconds until that policy’s window resets.

RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset are the older single-value form that most clients already read. They describe the tightest policy — the one closest to exhaustion — because they can only describe one.

A search from an anonymous caller with a full allowance:

RateLimit-Policy: "api-ip";q=600;w=60, "search-ip";q=5;w=60, "account-ip";q=10;w=900
RateLimit: "api-ip";r=599;t=60, "search-ip";r=5;t=60, "account-ip";r=10;t=900
RateLimit-Limit: 5
RateLimit-Remaining: 5
RateLimit-Reset: 60

Read that as: 599 requests left this minute overall, but only 5 uncached searches, and 10 account-backed lookups left in the current 15-minute window. Pace against search-ip, the one the compatibility triple picked out.

The same caller after spending the search burst:

RateLimit: "api-ip";r=594;t=43, "search-ip";r=0;t=17, "account-ip";r=10;t=900
RateLimit-Limit: 5
RateLimit-Remaining: 0
RateLimit-Reset: 17
Retry-After: 17
Cache-Control: no-store

The response is 429 with a rate_limited problem document. Retry-After and the exhausted policy’s t are the same number of seconds; if they ever disagree, Retry-After wins. Sleep for it — do not poll, and do not retry on a shorter interval hoping for a different answer. A 503 from live search carries Retry-After the same way.

The anonymous allowance is sized for interactive use. A script or service that searches all day will keep hitting it; ask for an API key instead, which brings its own search allowance. Keys are issued by hand: open an issue describing the workload, as auth.md explains.

The /api/* routes list all five, plus Retry-After, in Access-Control-Expose-Headers, so a browser client can read them cross-origin.

Cached responses lie about quota

x.md’s successful responses are cacheable, and a CDN replays the origin’s headers verbatim. On a cache hit those RateLimit values were computed for whoever caused the cache to fill, minutes ago, possibly from another IP.

Ignore RateLimit on any response with a positive Age header, which is what the specification tells clients to do. X-Cache and x-vercel-cache say the same thing in x.md’s own terms. Rejections are never cached — a 429 carries Cache-Control: no-store — so a 429 you receive is always about you.

If the shared counter store is unreachable, the front door fails open and reports a full quota — honest about the throttling actually in effect, which in that window is none. Account-backed search is the exception: it fails closed and answers 429 rather than risk overspending the shared pool.

Caching

Successful post conversions cache for about one hour by default. Profiles cache for 30 minutes, search and replies for 10 minutes, and follower lists for 6 hours. Degraded search results are rechecked after about one minute.

nocache=true skips x.md’s application cache. It cannot bypass a provider’s own cache. Use it when freshness matters, rather than on every request — a cached read is faster and costs you no search allowance.

X-Cache reports the application cache status and X-Source the provider that served the result.

Incomplete results

Latest and Top search can fall back to web-indexed snippets. Check degraded or X-Search-Degraded; snippets may be truncated, contain no metrics, and have different ordering. Photos, Videos, and Users require live search.

Public data and privacy

x.md is read-only. It does not post, follow, or like on your behalf. Public X Lists and private posts are not supported. Successful responses may be cached and served to other callers requesting the same resource.

Related: error catalogue · versioning and deprecation · response formats.

Last updated on

Was this page helpful?