Rate Limiting

Every endpoint on jekcms's REST API is rate-limited. The limits exist to protect the database and to stop runaway scripts from starving other clients on shared hosting. This page explains the default caps, how they're counted, how to detect and react to a limit, and how to raise them on the Enterprise tier.

Default limits

| Caller | Limit | |--------|-------| | Authenticated (API token) | 60 requests / minute | | Unauthenticated | 10 requests / minute | | Bulk operations (any authenticated caller) | 5 requests / minute |

"Bulk operations" means endpoints that touch many rows in one call — POST /posts/bulk, DELETE /comments/bulk, POST /media/bulk-import. These run heavier queries so they get a tighter cap that counts independently of the general 60/min.

How the limit is tracked

The counter is per API token, not per IP. Two clients sharing a token share the same budget. If you need independent budgets, issue separate tokens — there's no limit on how many tokens one account can hold.

The window is a 60-second sliding window. Every request records its timestamp; the rate is "requests in the trailing 60 seconds." This is more forgiving than a fixed window at clock-minute boundaries — a burst of 60 requests at 14:59:59 doesn't get paired with another 60 at 15:00:01 for a 120-request second.

Unauthenticated callers are tracked by client IP. Shared IPs (corporate NAT, a whole office behind one public IP) share the 10/min budget. If this is a problem, the fix is to authenticate.

The 429 response

When a caller exceeds its budget, the next request returns:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 37
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1713710057

{
  "error": "rate_limit_exceeded",
  "message": "You have exceeded the rate limit. Retry after 37 seconds.",
  "retry_after": 37
}

Key headers:

  • Retry-After — seconds until the oldest request in the window ages out and you'll have budget again
  • X-RateLimit-Limit — your effective cap (useful on custom/ENT tiers)
  • X-RateLimit-Remaining — how many you have left in the current window
  • X-RateLimit-Reset — Unix timestamp of next full-budget restoration

Always respect Retry-After. Hammering a rate-limited endpoint in a tight loop doesn't unblock it — it resets the window.

Every successful response also carries the three X-RateLimit-* headers, so well-behaved clients can proactively slow down as Remaining approaches zero.

On any 429:

import time, requests

def jekcms_request(method, url, **kwargs):
    for attempt in range(3):
        r = requests.request(method, url, **kwargs)
        if r.status_code != 429:
            return r
        wait = int(r.headers.get('Retry-After', 10))
        time.sleep(wait + 1)  # +1 for clock skew
    r.raise_for_status()
    return r

Most HTTP client libraries have a built-in retry-with-backoff option (requests's urllib3.Retry, Axios's axios-retry). Configure them to retry on 429 only, with a backoff driven by Retry-After.

Workarounds

Before asking for a higher limit, check whether you can get the same work done in fewer calls.

Use bulk endpoints for many reads

GET /posts?ids=1,2,3,...,100 returns 100 posts in one request. That's one slot on your budget, not 100. All list endpoints support ids= filters.

Cache on the client

Most data changes rarely. If you're fetching /categories on every page of your app, cache the result for an hour — that's a list of ~20 rows that almost never changes, and caching kills 90%+ of your API traffic instantly.

ETag and Last-Modified headers are returned on every GET. Your client can send If-None-Match / If-Modified-Since on the next call and get a cheap 304 Not Modified that does not count against your rate limit.

Batch writes into a single bulk call

Instead of 50 × POST /comments/{id}/approve, use one POST /comments/bulk with { "ids": [...], "action": "approve" }. Bulk operations cost one slot against the bulk budget, not 50 against the general one.

Split reads and writes across tokens

Issue one token for "read-only dashboard traffic" and another for "admin write operations." Each has its own 60/min budget, so a busy reporting cron won't starve your editor tools.

Scaling up on Enterprise tier

Enterprise-tier licenses can raise the limits. Typical ENT configurations:

| Caller | ENT default | |--------|-------------| | Authenticated | 600 req / min | | Bulk operations | 60 req / min | | Per-IP unauthenticated | 60 req / min |

Custom limits are configured server-side in config/config.php under RATE_LIMIT_* constants. ENT customers get access to the underlying knobs; support will walk you through sensible values for your workload.

Non-ENT customers who need a specific endpoint uncapped (for example, a webhook receiver that might burst) can add an IP allow-list under Settings → API → Rate-Limit Exemptions. Allow-listed IPs skip the counter entirely.

Be the first to know

New features, release notes & CMS guides — a couple of emails a month, no spam.