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).
| Plan | Price | Quota | Window |
|---|---|---|---|
| Free | Free | 100 | day |
| Pro | $3/mo | 10,000 | day |
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 1Quota-friendly patterns
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.
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.
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.
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
- 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.
- 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.
- 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.
- 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).
- 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.
- 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.