Rate Limits & Abuse

We have 3 layers of rate limiting, not 1

Most APIs say "rate limited" and don't explain how. We'll tell you exactly what happens at each layer.

Layer 1: Per-minute rate limit (per tier)

What it does: Redis atomic operation, enforces per-tier rate limits per minute.

Free Tier
10 requests/minute
Pro Tier
100 requests/minute
Enterprise
Custom
Response time
~180ms (Redis latency)
Window
Rolling 60 seconds

Example: Bot making 100 requests/second → Blocked after 10, doesn't hit Redis or database.

Error response:

429 Too Many Requests
{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMIT_MEMORY",
  "limit": 10,
  "window": "1 minute",
  "resetIn": 45,
  "hint": "You're making requests too quickly. Slow down."
}

Layer 2: Behavioral throttling — Slows down burst traffic

What it does: If you make ≥30 requests/minute, we add delays to slow you down (protect infrastructure).

30-49 requests/minute
+2 second delay
50+ requests/minute
+5 second delay

Example: You make 45 requests in 1 minute → Requests still succeed but each waits 2s.

Layer 3: Monthly quota (Redis-cached) — Prevent overage charges

What it does: Enforces monthly request limits per tier. Cached in Redis for instant checks (NO database queries!).

Free Tier
1,000 requests/month
Pro Tier
50,000 requests/month
Enterprise
Custom (millions)

Example: Free user hits 1,000/1,000 requests on Jan 25 → Blocked until Feb 1.

Error response:

403 Forbidden
{
  "error": "Monthly quota exceeded",
  "code": "QUOTA_EXCEEDED",
  "tier": "free",
  "used": 1000,
  "limit": 1000,
  "resetAt": "2024-02-01T00:00:00Z",
  "resetIn": 518400,
  "upgrade": {
    "url": "/dashboard/billing/upgrade",
    "nextTier": "pro",
    "nextLimit": 50000
  }
}

Chaos Protection — Ban escalation for persistent abuse

What it does: Tracks violations (rate limit hits, invalid requests) and automatically escalates bans for repeat offenders.

10 violations in 60 seconds
→ 5-minute ban
Get banned again (2nd offense)
→ 1-day ban
Get banned again (3rd offense)
→ 7-day ban
Get banned again (4th offense)
→ PERMANENT BAN

Why this matters: Makes persistent attacks EXPENSIVE. Bots can't just retry forever—each violation makes the next ban exponentially longer.

Permanent bans: Stored in database, enforced across all API endpoints, requires manual review to lift.

Layer 3: Database quota (20-50ms) — Monthly totals

What it does: Supabase query, checks total requests this billing cycle.

Free Tier
1,000 requests/month
Pro Tier
50,000 requests/month
Enterprise
Custom (millions)

Example: Free user hits 1,000/1,000 requests on Jan 25 → Blocked until Feb 1.

Error response:

403 Forbidden
{
  "error": "Monthly quota exceeded",
  "code": "QUOTA_EXCEEDED",
  "tier": "free",
  "used": 1000,
  "limit": 1000,
  "billingCycle": {
    "start": "2024-01-01T00:00:00Z",
    "end": "2024-02-01T00:00:00Z",
    "resetIn": 518400
  },
  "hint": "Upgrade to Pro for 50,000 requests/month or wait for reset",
  "upgrade": {
    "url": "/dashboard/billing/upgrade"
  }
}

How requests are counted

1 API call = 1 request, regardless of operation:

Generate signed URL
1 request
Upload completion hook
1 request
Delete file
1 request
List files
1 request
Batch upload (100 files)
1 request ⭐
Batch delete (50 files)
1 request ⭐

Important: Batch operations count as one request, regardless of file count. This is intentional—it encourages efficient API usage.

Limits on batch size:

  • Free: 10 files per batch
  • Pro: 100 files per batch
  • Enterprise: 10,000 files per batch

What happens when you hit a limit

⚠️ At 50% usage:
Email warning: "You've used 500/1,000 requests this month"
Dashboard banner: Shows usage with upgrade prompt
⚠️ At 80% usage:
Urgent email: "You're about to hit your limit"
Dashboard: Red banner with countdown
❌ At 100% usage:
API returns 403 Forbidden or 429 Too Many Requests
Response includes upgrade link and reset time
No surprise charges — just blocked until upgrade or reset

Abuse detection and prevention

Beyond rate limits, we track abuse events:

  • Rate limit exceeded (any layer)
  • Invalid credentials submitted repeatedly
  • Disposable email detected
  • Quota exceeded
  • Verification spam (clicking "verify" 100× in 1 minute)
  • Domain creation spam

Automatic actions:

10 abuse events
Account flagged (manual review)
50 abuse events
Account suspended (automatic)
100+ abuse events
IP banned (escalated to Cloudflare)

False positives: If you think you were suspended incorrectly, email support@obitox.com with:

  • Your API key (first 8 characters, e.g., ox_196ae...)
  • What you were trying to do
  • Any error messages/request IDs you received

We'll review logs and unban if it was a mistake. Most bans are correct (sorry, bots).

How to avoid hitting limits

1. Use batch operations
Uploading 100 files? Use batch signed URLs (1 request) instead of 100 separate calls.
2. Cache signed URLs
Signed URLs are valid for 1 hour by default. Generate once, use multiple times.
3. Implement client-side retry logic
If you get 429, wait for resetIn seconds before retrying.
if (response.status === 429) {
  const data = await response.json();
  await sleep(data.resetIn * 1000);
  return retry();
}
4. Monitor your usage
Dashboard shows current usage vs limits. Set up alerts at 80% to avoid surprises.
5. Upgrade proactively
Don't wait until you're blocked. If you're consistently hitting 90%+ usage, upgrade before it becomes a problem.

Cloudflare layer (Layer 0)

Before requests even hit our API, Cloudflare provides:

  • DDoS protection — Blocks 182 billion threats/day globally
  • Bot detection — Challenge Score / Turnstile
  • IP reputation — Known bad IPs blocked automatically
  • L7 rate limiting — 1,000 requests/min per IP

This catches ~90% of attacks before they reach our code. If you're a legitimate user, you'll never notice it. If you're a bot, you'll never get through.

Response headers (for monitoring)

Every successful request includes rate limit headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1704675600
X-RateLimit-Tier: pro

Use these to monitor usage in your application:

const response = await fetch(...);
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));

if (remaining < 100) {
  console.warn('Running low on requests:', remaining);
  // Maybe slow down, notify admins, etc.
}