Skip to content
On this page

Rate Limits & Quotas

Spooled Cloud enforces rate limits and quotas to ensure fair usage and platform stability. Limits vary by plan tier and can be increased for Enterprise customers.

Plan Comparison

Resource FreeStarterProEnterprise
Jobs per day 1,00010,000100,000Unlimited
Active jobs 105005,000Unlimited
Queues 21050Unlimited
Workers 1525Unlimited
API keys 2525Unlimited
Schedules 11050Unlimited
Workflows 525Unlimited
Webhooks 1520Unlimited
Max payload size 64 KB256 KB1 MB1 MB
API rate (req/sec) 525100500
API burst 10502001000
Job retention 3 days14 days30 days90 days
History retention 1 days7 days30 days90 days

Job retention is how long completed and cancelled jobs are kept. History retention covers the supporting records — including outgoing webhook delivery history — which a per-organization sweep deletes once they pass the window. On Free that is a single day, so pull anything you need for debugging the same day it happens.

Rate Limiting

API rate limits use a sliding window algorithm. When you exceed the limit, requests return 429 Too Many Requests with a Retry-After header (seconds to wait before retrying):

HTTP/1.1 429 Too Many Requests
Retry-After: 12

Handling Rate Limits

Production Ready: All official SDKs (Node.js, Python, Go, PHP) include built-in rate limit handling with automatic retries. See the SDK documentation for details.

import { SpooledClient, RateLimitError } from '@spooled/sdk';

const client = new SpooledClient({ apiKey: process.env.SPOOLED_API_KEY! });

async function createJobWithRetry(payload, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.jobs.create(payload);
    } catch (error) {
      if (error instanceof RateLimitError) {
        const retryAfter = error.getRetryAfter();
        console.log(`Rate limited, retrying in ${retryAfter}s...`);
        await sleep(retryAfter * 1000);
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

Quota Enforcement

Quotas are enforced at different granularities. Exceeding a plan resource limit returns 429 Too Many Requests with a stable body shape (field values vary by resource):

{
  "error": "limit_exceeded",
  "code": "QUOTA_EXCEEDED",
  "message": "daily job limit reached (1000/1000). Upgrade to starter for higher limits.",
  "resource": "jobs_per_day",
  "current": 1000,
  "limit": 1000,
  "plan": "free",
  "upgrade_to": "starter"
}

resource values the API can return, and the endpoints that trigger each:

resource What it caps Triggered by
jobs_per_day Jobs created today (UTC midnight reset) POST /api/v1/jobs, POST /api/v1/jobs/bulk, gRPC Enqueue, schedule/cron trigger, DLQ retry, inbound webhook enqueue
active_jobs Pending + processing jobs Same job-creation paths as jobs_per_day
queues Queue count POST /api/v1/queues
workers Registered workers POST /api/v1/workers/register, gRPC worker register
api_keys API keys POST /api/v1/api-keys
schedules Cron schedules POST /api/v1/schedules
workflows Workflow DAGs (disabled on free) POST /api/v1/workflows
webhooks Outgoing webhook endpoints POST /api/v1/outgoing-webhooks, PUT /api/v1/outgoing-webhooks/{id} when it re-enables a disabled endpoint

The workers cap counts registered rows, not running processes. A worker that restarts without a stable worker_id registers a new row and abandons the old one, which keeps holding a slot until the stale-worker reaper clears it (about two minutes) — on a tight plan a crash-looping worker can lock itself out of registering. Pass a stable worker_id and registration becomes an upsert: the worker reuses its row, and re-registering an id you already own is not charged against the cap. See worker registration.

An outgoing webhook that was auto-disabled after 20 consecutive failed deliveries is turned back on with PUT /api/v1/outgoing-webhooks/{id} and body {"enabled": true}. That request is charged against this cap like a creation, so at the cap it returns 429 QUOTA_EXCEEDED instead of restoring the endpoint.

Payload Size

Job payloads exceeding the plan limit are rejected with 413 (not the QUOTA_EXCEEDED shape above):

{
  "error": "payload_too_large",
  "code": "PAYLOAD_TOO_LARGE",
  "message": "Payload size (128000 bytes) exceeds plan limit (65536 bytes). Upgrade for larger payloads.",
  "current": 128000,
  "limit": 65536,
  "plan": "free",
  "upgrade_to": true
}

Automatic Limit Enforcement

Spooled automatically enforces plan limits before operations are executed, ensuring you never exceed your quotas unexpectedly.

Operations with Automatic Enforcement

Job Creation
  • • POST /api/v1/jobs
  • • POST /api/v1/jobs/bulk
  • • gRPC Enqueue
Workflows
  • • POST /api/v1/workflows
  • • Counts all jobs in DAG
Schedules
  • • POST /api/v1/schedules/trigger
  • • Automatic cron triggers
DLQ Operations
  • • POST /api/v1/jobs/dlq/retry
  • • Validates before reactivating

For gRPC operations, the status code is RESOURCE_EXHAUSTED with the same error details in the message.

Monitoring Usage

Track your usage via the API or dashboard:

# Get current usage and plan limits
curl https://api.spooled.cloud/api/v1/organizations/usage \
  -H "Authorization: Bearer YOUR_API_KEY"

# Response
{
  "plan": {
    "tier": "starter",
    "display_name": "Starter"
  },
  "usage": {
    "jobs_today": {
      "current": 1250,
      "limit": 10000,
      "percentage": 12.5
    },
    "active_jobs": {
      "current": 45,
      "limit": 500,
      "percentage": 9.0
    },
    "queues": { "current": 8, "limit": 10 },
    "workers": { "current": 3, "limit": 5 }
  }
}

The response includes job counts, resource usage, and API call statistics for the current period.

Increasing Limits

Upgrade Your Plan

The easiest way to get higher limits is to upgrade to a paid plan at /pricing.

Enterprise Custom Limits

Enterprise customers can negotiate custom limits. Contact [email protected] to discuss your requirements.

Self-Hosted

Self-hosted deployments can configure plan limits via environment variables on the backend. The values shown above are Spooled Cloud defaults; your self-hosted instance may differ.

Backend env vars: configure plan defaults with SPOOLED_PLAN_LIMITS_JSON, SPOOLED_PLAN_<TIER>_LIMITS_JSON, or per-field SPOOLED_PLAN_<TIER>_* variables. Per-org DB overrides (custom_limits) still take highest precedence. See spooled-backend/.env.example for the full list and precedence.

Best Practices

  • Batch operations — Use bulk endpoints (/jobs/bulk) to reduce API calls
  • Implement backoff — Use exponential backoff when rate limited
  • Monitor usage — Set up alerts before hitting quotas
  • Use idempotency keys — Safely retry without creating duplicates
  • Consolidate payloads — Keep payloads small to stay within limits

Need Higher Limits?

Last updated 2026-09-10