Loopwise Docs
Admin API

Rate Limiting

Understanding rate limits and best practices for the Loopwise Admin API

Note: The Loopwise Admin API is currently under development and not yet available for public use. This documentation is provided for preview purposes only.

Overview

The Loopwise Admin API implements rate limiting to ensure system stability and fair usage across all users. Rate limits are applied per API key and vary based on the type of operation being performed.

Rate Limit Tiers

Rate limits are differentiated by operation type to balance system resources while providing a good developer experience:

Operation TypeLimitWindowTypical Use Cases
Mutations200 requestsper minuteCreating, updating, or deleting resources
Queries600 requestsper minuteReading data, listing resources

These limits are designed to accommodate:

  • Batch operations (e.g., enrolling multiple students)
  • Data synchronization workflows
  • Dashboard and reporting applications
  • Real-time data queries

How Rate Limiting Works

Identification

Rate limits are tracked per API key. Each API key has its own independent rate limit counters for mutations and queries.

Time Windows

Rate limits use a sliding window of 1 minute:

  • The counter tracks requests in the past 60 seconds
  • Once a request is older than 60 seconds, it no longer counts toward your limit
  • This provides smooth, continuous access rather than hard resets at fixed intervals

Operation Detection

The API automatically detects whether a request is a mutation or query:

  • Mutation: GraphQL query string starts with mutation
  • Query: All other GraphQL operations (including introspection queries)

Example:

# This counts as a MUTATION (200/minute limit)
mutation {
  enrollStudentToCourse(input: { ... }) {
    enrollment { id }
  }
}

# This counts as a QUERY (600/minute limit)
query {
  courses(first: 50) {
    nodes { id name }
  }
}

Rate Limit Headers

When you exceed the rate limit, the API returns a 429 status code with headers to help you track your usage:

Response Headers (429 Only)

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 45
X-RateLimit-Limit: 200
X-RateLimit-Reset: 1699012860
  • Retry-After: Number of seconds you should wait before making another request
  • X-RateLimit-Limit: The maximum number of requests allowed in the current window
  • X-RateLimit-Reset: Unix timestamp indicating when the rate limit window resets

Error Response Format

When you exceed a rate limit, you'll receive a structured GraphQL error response:

{
  "errors": [{
    "message": "Rate limit exceeded for mutations. Please retry after 45 seconds.",
    "extensions": {
      "code": "RATE_LIMIT_EXCEEDED",
      "operation_type": "mutations",
      "limit": 200,
      "retry_after": 45,
      "reset_at": 1699012860
    }
  }]
}

Error Response Fields

FieldDescription
messageHuman-readable error message
codeError code: RATE_LIMIT_EXCEEDED
operation_typeEither mutations or queries
limitThe rate limit that was exceeded
retry_afterSeconds to wait before retrying
reset_atUnix timestamp when the limit resets

Concurrency and Execution-Time Limits

Request counts do not capture how expensive a GraphQL operation is, so a second layer limits how much work one credential can have in progress. It applies to every credential type (API keys, OAuth applications, vendor apps, installed apps), not only API keys.

BudgetLimitNotes
Operations in flight10 at a timeThe 11th concurrent operation is rejected until one finishes
Execution time60 seconds per 60-second windowA token bucket charged with each operation's server-side execution time; it refills continuously, so one 4-second operation costs the same as forty 100 ms ones

Enforcement is enabled per credential. Until it is enabled for yours, an operation over either budget still runs and the response is 200; the RateLimit headers below are reported either way, so you can bring your usage under the budgets before enforcement reaches you.

Headers (every executed operation)

Unlike the per-minute limits, these budgets are reported on every response whose operation reached execution — admitted or rejected — so you can pace yourself before being rejected. The values describe the budgets after that operation was charged:

RateLimit-Policy: "active-operations";q=10;qu="concurrent-requests"
RateLimit: "active-operations";r=9
RateLimit-Limit: 60000
RateLimit-Remaining: 58240
RateLimit-Reset: 2
  • RateLimit-Policy / RateLimit: the concurrency budget, in the structured fields of the IETF RateLimit header draft — q is the quota, qu its registered unit, r the operations you can still start
  • RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset: the execution-time budget under the older header names — the budget in milliseconds, what is left of it, and the seconds until it is full again. The draft has no quota unit for execution time, which is why this budget is not in RateLimit-Policy

Responses that never reach the limiters — authentication failures, an invalid API-Version, GraphQL syntax or validation errors — carry no budget headers. All of these fields are exposed to cross-origin browser clients.

Rejection

A rejected operation is not executed. The response status is 429, Retry-After says how long to wait (1 second for a concurrency rejection, the refill time for an exhausted execution budget), and the body is a regular GraphQL errors payload so GraphQL clients see the same error:

{
  "errors": [{
    "message": "Rate limited by runtime: limit 60000",
    "extensions": {
      "code": "RATE_LIMITED",
      "limiter": "runtime",
      "retry_after_ms": 1820
    }
  }]
}
FieldDescription
codeError code: RATE_LIMITED
limiteractive_operations or runtime — which budget rejected the operation
retry_after_msMilliseconds until the execution budget admits an operation again; present for runtime only

Tell the two layers apart by the error code: RATE_LIMIT_EXCEEDED is the per-minute request count, RATE_LIMITED is a concurrency or execution-time budget. Both send Retry-After, so a client that honours that header handles both.

Staying under the execution budget

  • Keep operations small: paginate with 50–100 items and avoid deeply nested selections
  • Do not run more than a handful of requests in parallel per credential
  • Watch RateLimit-Remaining; when it drops low, slow down instead of waiting for a 429
  • Expensive mutations (for example enrolling a member into a plan with many courses) can take seconds each — batch them sequentially, not concurrently

Best Practices

To avoid hitting rate limits:

  • Respect the Retry-After header - Always wait the specified time before retrying a 429 response
  • Implement retry logic - Use exponential backoff with a maximum retry limit for production applications
  • Cache query responses - Reduce API calls by caching frequently accessed data
  • Batch operations - Use GraphQL aliases to combine multiple mutations into a single request
  • Use appropriate pagination - Fetch 50-100 items per page instead of making many small requests
  • Spread out non-urgent operations - Add delays between background tasks to stay under limits

Common Scenarios

High-Volume Data Synchronization: Use pagination (50-100 items/page) with 1-2 second delays between requests. Cache data when possible and run during off-peak hours.

Real-Time Dashboards: Cache aggressively (30-60 seconds), use webhooks instead of polling when available, and implement smart polling intervals.

Batch Operations: Group related operations into single requests using GraphQL aliases. Process in batches of 20-50 items with delays between batches.

Increasing Rate Limits

The default rate limits are designed to accommodate the vast majority of use cases. However, if you have a legitimate need for higher limits:

  1. Contact our support team with details about your use case
  2. Provide information about:
    • Your application's purpose
    • Expected request patterns
    • Peak usage times
    • Number of users/schools affected
  3. Demonstrate that you've implemented best practices (caching, batching, etc.)

We review rate limit increase requests on a case-by-case basis.

Configuration

For self-hosted deployments, rate limits can be adjusted in config/initializers/rack_attack.rb:

module AdminGraphqlRateLimits
  MUTATIONS_LIMIT = 200  # requests per minute (default: 200)
  QUERIES_LIMIT = 600    # requests per minute (default: 600)
  PERIOD = 1.minute      # time window
end

After modifying these values, restart your Rails server for the changes to take effect.

Testing Rate Limits

When developing your integration, you can test rate limit handling:

# Test script to trigger rate limits (for testing only)
for i in {1..250}; do
  curl -X POST https://teachify.io/admin/graphql \
    -H "Content-Type: application/json" \
    -H "X-Teachify-API-Key: your_test_key" \
    -d '{"query":"mutation { enrollStudentToCourse(input: {courseId: \"test\", email: \"test@example.com\"}) { enrollment { id } } }"}' \
    &
done

Note: Only test rate limits in a development or staging environment, never in production.

FAQ

Q: Are rate limits shared across multiple API keys?

No. Each API key has its own independent rate limit counters.

Q: Do failed requests count toward my rate limit?

Yes. All requests count toward your rate limit, regardless of whether they succeed or fail.

Q: Can I check my current rate limit usage without making a request?

No. Rate limit information is only provided in response headers after making a request.

Q: What happens to requests made exactly when the limit resets?

The sliding window approach means there's no hard reset. As older requests age out (after 60 seconds), your available quota gradually increases.

Q: Are GraphQL introspection queries counted?

Yes, introspection queries count as queries and use your query rate limit (600/min).

Q: Do retried requests count multiple times?

Yes. Each request attempt counts toward your limit, including retries.

Summary

  • Mutations: 200/minute per API key
  • Queries: 600/minute per API key
  • In flight: 10 operations at a time per credential
  • Execution time: 60 seconds per 60-second window per credential, reported on every executed operation via the RateLimit-* headers
  • Always respect the Retry-After header when receiving a 429 response
  • Implement retry logic with exponential backoff
  • Cache responses and batch operations to reduce API calls

On this page