Rate Limits

The Udacity Public API enforces rate limits to ensure fair usage and protect service stability. Limits are applied per identity (authenticated user or API token) and per IP for unauthenticated requests.

Rate limits apply across all API domains (Catalog, Program Progress, Assessment Progress, Learning Plan Progress, Tokens). A single token’s requests to different domains all count toward the same limit.

Current Limits

Authenticated requests

100 requests per minute per identity

Unauthenticated requests

100 requests per minute per IP address

Rate limits apply across all API domains. A single token’s requests to different domains all count toward the same limit.

Need higher limits? If your use case requires higher rate limits, contact your Udacity account team to discuss options.

Rate Limit Headers

Every response includes headers to help you track your usage:

X-RateLimit-Limitinteger

Maximum requests allowed per window

X-RateLimit-Remaininginteger

Requests remaining in the current window

Retry-Afterinteger

Seconds to wait before retrying (only on 429 responses)

Example Response Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
Content-Type: application/json

When You Hit the Limit

If you exceed the rate limit, the API returns a 429 Too Many Requests response with the Retry-After header indicating how many seconds to wait.

429 Response
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Retry-After: 60
Content-Type: application/json
Response Body
{
  "error": {
    "message": "Rate limit exceeded. Please try again later.",
    "code": "RATE_LIMIT_EXCEEDED"
  }
}

Retry Strategy

When you receive a 429 response, implement exponential backoff with jitter. This spreads retries over time and prevents thundering-herd effects.

JavaScript
async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    if (attempt === maxRetries) {
      throw new Error('Rate limit exceeded after max retries');
    }

    // Exponential backoff with jitter
    const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
    const jitter = Math.random() * 1000;
    const delay = baseDelay + jitter;

    await new Promise(resolve => setTimeout(resolve, delay));
  }
}

Best Practices

Monitor the X-RateLimit-Remaining header

Proactively slow down before hitting the limit.

Optimize your queries

  • Request only the fields you need to reduce server processing time
  • Use pagination with reasonable page sizes (100–500) instead of requesting everything at once
  • Cache responses when the data doesn’t change frequently
  • Batch related data into a single GraphQL query instead of multiple requests

Use appropriate page sizes

Instead of making many small requests, make fewer, larger requests.

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

if (remaining < 10) {
  // Approaching the limit — add a delay
  await new Promise(resolve => setTimeout(resolve, 2000));
}
Page Size
# Bad: 1000 requests for 1000 records (hits rate limit)
query { programProgress(input: { first: 1 }) { edges { node { ... } } } }

# Good: 10 requests for 1000 records
query { programProgress(input: { first: 100 }) { edges { node { ... } } } }