Skip to content

Reliability

Markdown API rate limits

The Markdown Viewer API uses two complementary rate limits: a per-key burst limiter that stops accidental loops, and a per-plan daily quota that gives you predictable costs. This page explains both, the response headers they emit, and how to handle 429 responses gracefully in production.

Per-plan request quotas

Each plan has a request budget that resets on a fixed window. Both the Free and Pro plans reset every 24 hours (UTC).

PlanPriceQuotaWindow
FreeFree100day
Pro$3/mo10,000day

Need more headroom? Compare plans on the API page or upgrade from billing.

Burst protection

On top of the daily quota, every key is limited to 20 requests per 10 seconds. This shields your quota from runaway loops and bursts of accidental traffic — and gently nudges you toward batching.

Bursts trip the limiter with error code rate_limited. The response includes a Retry-After header telling you how many seconds to wait.

Response headers

Every API response — successful or not — includes the headers below so you can keep an eye on your remaining quota and react before you hit a 429.

X-RateLimit-Limit:     10000          ← total quota for the window
X-RateLimit-Remaining: 9897           ← requests left in the window
X-RateLimit-Reset:     1727894400     ← unix epoch when the window resets
X-RateLimit-Window:    day            ← "day" or "month" depending on plan
Retry-After:           42             ← seconds to wait (only on 429 responses)

Use these to surface remaining capacity in your dashboard or to back off proactively before exhausting your quota.

Handling 429 in production

When you receive a 429, inspect error.code: if it's rate_limited the wait is short (seconds); if it's quota_exceeded you'll need to wait for the window reset or upgrade. Always honour Retry-After, and add jitter to avoid synchronised retry storms.

TypeScript

async function callWithRetry(fn: () => Promise<Response>, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fn();
    if (res.status !== 429) return res;
    const retryAfter = Number(res.headers.get("Retry-After") ?? "1");
    const jitter = Math.random() * 0.3;
    await new Promise((r) => setTimeout(r, retryAfter * 1000 * (1 + jitter)));
  }
  throw new Error("Rate limited after retries");
}

Python

import random, time, requests

def call_with_retry(make_request, attempts: int = 3):
    for _ in range(attempts):
        response = make_request()
        if response.status_code != 429:
            return response
        retry_after = float(response.headers.get("Retry-After", "1"))
        jitter = random.random() * 0.3
        time.sleep(retry_after * (1 + jitter))
    raise RuntimeError("Rate limited after retries")

curl + bash

#!/usr/bin/env bash
for i in 1 2 3; do
  response=$(curl -sS -w "\n%{http_code}" -X POST "$ENDPOINT" \
    -H "Authorization: Bearer $MDV_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"markdown": "# hi"}')
  status="${response##*$'\n'}"
  if [ "$status" != "429" ]; then
    echo "$response"
    exit 0
  fi
  sleep "${RETRY_AFTER:-1}"
done
echo "Rate limited after retries" >&2
exit 1

Quota-friendly patterns

Pattern

Cache rendered HTML by content hash

If the same markdown is rendered repeatedly (eg. blog posts, READMEs), cache the response keyed by a SHA of the input. You only pay the API once per unique input.

Pattern

Render on write, not on read

For user-generated content, render to HTML when the user saves the post, then store the result. Reads serve cached HTML — your quota tracks edits, not pageviews.

Pattern

Validate at edit time

Run /api/v1/validate on form submission to surface markdown issues in the editor. Skip the call on previews that are still being typed — debounce by 500-1000ms.

Pattern

Surface remaining quota in the UI

Pipe the X-RateLimit-Remaining header to your dashboard or an internal monitoring channel. Set an alert at ~10% remaining so you can upgrade before users feel it.

Rate-limit FAQ

What's the rate limit on the free plan?
100 requests per day. The counter resets at midnight UTC. The free plan also has the same 20 requests / 10 seconds burst limit as every other plan.
Does the burst limit count against my plan quota?
No. The burst limiter (20 req / 10s) is independent of the daily or monthly plan quota. It exists to prevent accidental runaway loops from chewing through your allowance.
What is the difference between rate_limited and quota_exceeded?
Both return HTTP 429. rate_limited means you tripped the 20 req / 10s burst limit and can retry shortly. quota_exceeded means the daily (or monthly) plan quota is fully used up — wait for the window to reset or upgrade your plan.
How should I retry after a 429?
Always honour the Retry-After response header. For network errors and 5xx responses, use exponential backoff with jitter and a small attempt cap (typically 3-5).
Do failed requests count toward my quota?
4xx errors caused by your client (bad_request, unauthorized, validation, payload_too_large, etc.) don't count against the daily/monthly quota. They do count toward the burst limiter to deter brute-force attempts.
Can I monitor my remaining quota?
Yes — every API response includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and X-RateLimit-Window headers. The dashboard usage page also shows live charts and the reset time.

Related