Rate Limits
lnkify applies per-identity rate limiting with 1-minute sliding windows.
Identity Resolution
For rate limit purposes, the server identifies the caller in this order:
| Interface | Priority Chain |
|---|---|
| GraphQL | apiKeyId → userId → IP address |
| MCP | apiKeyId → userId → IP address (separate bucket from GraphQL) |
| Redirects | IP address only |
Default Limits
| Bucket | Default | Env Variable |
|---|---|---|
| GraphQL requests | 120/min | RATELIMIT_GRAPHQL_PER_MIN |
| MCP requests | 120/min | RATELIMIT_MCP_PER_MIN |
| Redirect follows | 600/min | RATELIMIT_REDIRECT_PER_MIN |
| Bulk operations | 10/min | RATELIMIT_BULK_PER_MIN |
| Auth mutations | 5/min and 20/hour | AUTH_RATE_LIMIT_MAX, AUTH_RATE_LIMIT_HOUR_MAX |
Bulk operations use a separate, stricter bucket. Individual createLnkify calls count against the GraphQL bucket; createLnkifies, updateLnkifies, and deleteLnkifies count against the bulk bucket.
Defaults vs. plan-based limits
The table above lists the server defaults (the RATELIMIT_* env vars). A default self-host runs on these. When billing enforcement is enabled (BILLING_ENFORCE=true), the GraphQL and MCP limits become plan-based via entitlements — roughly 60/min on FREE, 120/min on STARTER, and 240/min on PRO. See Billing & Usage.
Auth mutations
login, signup, and resetPassword ride a dedicated, much stricter per-IP bucket on top of the GraphQL limit — by default 5 attempts/minute and 20/hour — to blunt credential stuffing, signup spam, and password-reset abuse. The window and caps are configurable via AUTH_RATE_LIMIT_WINDOW_MINUTES / AUTH_RATE_LIMIT_MAX and AUTH_RATE_LIMIT_HOUR_WINDOW_MINUTES / AUTH_RATE_LIMIT_HOUR_MAX.
Response Headers
Every response includes rate limit headers:
RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 1718234567| Header | Meaning |
|---|---|
RateLimit-Limit | Maximum requests allowed in the window |
RateLimit-Remaining | Requests remaining in the current window |
RateLimit-Reset | Unix timestamp when the window resets |
429 Response
When the limit is exceeded, the server returns HTTP 429 with a Retry-After header:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{
"errors": [{
"message": "Too many requests, please try again later.",
"extensions": { "code": "RATE_LIMITED" }
}]
}Client Handling
Use exponential backoff with jitter when hitting rate limits:
async function graphqlRequest(query, variables, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
const response = await fetch("https://lnkify.io/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "lf_live_YOUR_KEY"
},
body: JSON.stringify({ query, variables })
});
if (response.status !== 429) return response.json();
const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
const delay = (attempt === 0) ? retryAfter * 1000 :
Math.min(retryAfter * 1000 * Math.pow(2, attempt), 60000);
await new Promise(r => setTimeout(r, delay + Math.random() * 1000));
}
throw new Error("Rate limit exceeded after retries");
}MCP Rate Limits
MCP requests use their own dedicated bucket (RATELIMIT_MCP_PER_MIN, default 120/min), separate from the GraphQL bucket and keyed by the same apiKeyId → userId → IP chain. Each tool call counts as one request. If you use multiple API keys (one per AI agent), each key gets its own limit.
Separately, the MCP server caps how many concurrent sessions a single identity (and the server as a whole) may hold open — see Scaling and the MCP_MAX_SESSIONS* / MCP_SESSION_TTL_MS settings.