Skip to content

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:

InterfacePriority Chain
GraphQLapiKeyId → userId → IP address
MCPapiKeyId → userId → IP address (separate bucket from GraphQL)
RedirectsIP address only

Default Limits

BucketDefaultEnv Variable
GraphQL requests120/minRATELIMIT_GRAPHQL_PER_MIN
MCP requests120/minRATELIMIT_MCP_PER_MIN
Redirect follows600/minRATELIMIT_REDIRECT_PER_MIN
Bulk operations10/minRATELIMIT_BULK_PER_MIN
Auth mutations5/min and 20/hourAUTH_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
HeaderMeaning
RateLimit-LimitMaximum requests allowed in the window
RateLimit-RemainingRequests remaining in the current window
RateLimit-ResetUnix timestamp when the window resets

429 Response

When the limit is exceeded, the server returns HTTP 429 with a Retry-After header:

http
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:

js
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.

Released under the MIT License.