Rate Limiting
Note: The Teachify Admin API is currently under development and not yet available for public use. This documentation is provided for preview purposes only.
Overview
Section titled “Overview”The Teachify 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
Section titled “Rate Limit Tiers”Rate limits are differentiated by operation type to balance system resources while providing a good developer experience:
| Operation Type | Limit | Window | Typical Use Cases |
|---|---|---|---|
| Mutations | 200 requests | per minute | Creating, updating, or deleting resources |
| Queries | 600 requests | per minute | Reading 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
Section titled “How Rate Limiting Works”Identification
Section titled “Identification”Rate limits are tracked per API key. Each API key has its own independent rate limit counters for mutations and queries.
Time Windows
Section titled “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
Section titled “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(page: 1, perPage: 50) { nodes { id name } }}Rate Limit Headers
Section titled “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)
Section titled “Response Headers (429 Only)”HTTP/1.1 429 Too Many RequestsContent-Type: application/jsonRetry-After: 45X-RateLimit-Limit: 200X-RateLimit-Reset: 1699012860Retry-After: Number of seconds you should wait before making another requestX-RateLimit-Limit: The maximum number of requests allowed in the current windowX-RateLimit-Reset: Unix timestamp indicating when the rate limit window resets
Error Response Format
Section titled “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
Section titled “Error Response Fields”| Field | Description |
|---|---|
message | Human-readable error message |
code | Error code: RATE_LIMIT_EXCEEDED |
operation_type | Either mutations or queries |
limit | The rate limit that was exceeded |
retry_after | Seconds to wait before retrying |
reset_at | Unix timestamp when the limit resets |
Concurrency and Execution-Time Limits
Section titled “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.
| Budget | Limit | Notes |
|---|---|---|
| Operations in flight | 10 at a time | The 11th concurrent operation is rejected until one finishes |
| Execution time | 60 seconds per 60-second window | A 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)
Section titled “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=9RateLimit-Limit: 60000RateLimit-Remaining: 58240RateLimit-Reset: 2RateLimit-Policy/RateLimit: the concurrency budget, in the structured fields of the IETF RateLimit header draft —qis the quota,quits registered unit,rthe operations you can still startRateLimit-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 inRateLimit-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
Section titled “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 } }]}| Field | Description |
|---|---|
code | Error code: RATE_LIMITED |
limiter | active_operations or runtime — which budget rejected the operation |
retry_after_ms | Milliseconds 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
Section titled “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 a429 - Expensive mutations (for example enrolling a member into a plan with many courses) can take seconds each — batch them sequentially, not concurrently
Best Practices
Section titled “Best Practices”To avoid hitting rate limits:
- Respect the
Retry-Afterheader - 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
Section titled “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
Section titled “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:
- Contact our support team with details about your use case
- Provide information about:
- Your application’s purpose
- Expected request patterns
- Peak usage times
- Number of users/schools affected
- Demonstrate that you’ve implemented best practices (caching, batching, etc.)
We review rate limit increase requests on a case-by-case basis.
Configuration
Section titled “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 windowendAfter modifying these values, restart your Rails server for the changes to take effect.
Testing Rate Limits
Section titled “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 } } }"}' \ &doneNote: Only test rate limits in a development or staging environment, never in production.
Q: Are rate limits shared across multiple API keys?
Section titled “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?
Section titled “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?
Section titled “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?
Section titled “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?
Section titled “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?
Section titled “Q: Do retried requests count multiple times?”Yes. Each request attempt counts toward your limit, including retries.
Summary
Section titled “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-Afterheader when receiving a 429 response - Implement retry logic with exponential backoff
- Cache responses and batch operations to reduce API calls