Skip to main content
For developers looking to optimize their Privy integration, we have a few key features that should help fine-tune the performance your setup.

Manually set a verification key for authorization

When verifying a Privy access token to authorize requests to your servers, by default the Privy Client’s verifyAuthToken method will make a request to Privy’s API to fetch the verification key for your app. Although it is cached for reuse, you can avoid this API request entirely by copying your verification key from the Configuration > App settings > Basics tab of the Dashboard, under “Verify with key instead”:
import {PrivyClient} from '@privy-io/node';

const privy = new PrivyClient({
  appId: 'your-privy-app-id',
  appSecret: 'your-privy-app-secret',
  // Set the verification key from the Dashboard when initializing the PrivyClient
  jwtVerificationKey: 'paste-your-verification-key-from-the-dashboard'
});
If you ever rotate your verification key, you will have to update this, but this will remove any network dependency on Privy for token verification.

Get user data with identity tokens

If you need access to the user object, especially on the server, this can be a costly action. To remove a network call from your critical path, we recommend using Privy’s identity tokens, which include the latest user information in token form. While it does not have the full user details (it omits certain lesser-needed fields for efficiency), it should have what you need to get started quickly.

Set a custom API URL for HttpOnly cookies (react-auth only)

In the case where you have set up and enabled HttpOnly cookies, on initial page load, the Privy SDK will start by making a call to fetch app details on our default https://auth.privy.io API URL. In HttpOnly cookie mode however, all your requests are routed through https://privy.<customdomain.com>. To avoid an occasional extra call on page load, we recommend explicitly setting the apiUrl in your PrivyProvider:
return (
  <PrivyProvider
    appId={'your-app-ID'}
    // @ts-expect-error currently a beta feature
    apiUrl="https://privy.customdomain.com"
  >
    {children}
  </PrivyProvider>
);
Note that this has a risk - if you are ever disabling HttpOnly cookies, you will need to remove this in order for your app to continue functioning properly. For a smooth transition, first remove the apiUrl, deploy, and then disable HttpOnly cookies.

Handling rate limits

When your application encounters rate limiting (HTTP 429 responses), implementing proper retry logic ensures a smooth user experience and optimal API usage.

Understanding rate limit responses

When you exceed a rate limit, Privy’s API returns a 429 Too Many Requests status code. Rate limits are applied per endpoint and are designed to ensure fair usage across all applications.

Best practices for handling rate limits

1. Implement exponential backoff

Exponential backoff is a standard error-handling strategy that gradually increases the wait time between retry attempts:
async function makeRequestWithRetry(
  requestFn: () => Promise<any>,
  maxRetries: number = 5,
  baseDelay: number = 1000
): Promise<any> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await requestFn();
    } catch (error: any) {
      // Check if it's a rate limit error
      if (error.status === 429 && attempt < maxRetries - 1) {
        // Calculate exponential backoff delay: 1s, 2s, 4s, 8s, 16s
        const delay = baseDelay * Math.pow(2, attempt);

        // Add jitter to prevent thundering herd
        const jitter = Math.random() * 1000;

        console.log(`Rate limited. Retrying in ${delay + jitter}ms...`);
        await new Promise((resolve) => setTimeout(resolve, delay + jitter));
      } else {
        throw error;
      }
    }
  }
  throw new Error('Max retries exceeded');
}

// Usage example
const user = await makeRequestWithRetry(() => privy.users()._get('did:privy:xxxxx'));

2. Batch your requests

Instead of making individual API calls for each operation, batch multiple operations together when possible:
// ❌ Avoid: Multiple individual requests
for (const userId of userIds) {
  const user = await privy.users()._get(userId);
  processUser(user);
}

// ✅ Better: Use list endpoint with pagination
for await (const user of privy.users().list()) {
  if (userIds.includes(user.id)) {
    processUser(user);
  }
}
For bulk user operations, use the batch user creation endpoint which allows creating up to 100 users per request.

3. Cache responses when appropriate

For data that doesn’t change frequently, implement caching to reduce API calls:
const userCache = new Map<string, {user: any; timestamp: number}>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getCachedUser(userId: string) {
  const cached = userCache.get(userId);

  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.user;
  }

  const user = await privy.users()._get(userId);
  userCache.set(userId, {user, timestamp: Date.now()});

  return user;
}

4. Use identity tokens for authenticated users

For getting user data about authenticated users, use identity tokens instead of making API calls. This approach is rate-limit-free and provides user information directly from the token.
// ✅ Preferred: Use identity token (no API call)
const user = await privy.users().get({id_token: idToken});

// ❌ Avoid when possible: Direct API call (subject to rate limits)
const user = await privy.users()._get(userId);

5. Implement circuit breakers

For production applications, consider implementing a circuit breaker pattern to temporarily stop making requests when rate limits are consistently hit:
class CircuitBreaker {
  private failureCount = 0;
  private lastFailureTime = 0;
  private readonly threshold = 3;
  private readonly cooldown = 60000; // 1 minute

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    // Check if circuit is open
    if (this.failureCount >= this.threshold) {
      const timeSinceLastFailure = Date.now() - this.lastFailureTime;
      if (timeSinceLastFailure < this.cooldown) {
        throw new Error('Circuit breaker is open. Too many rate limit errors.');
      }
      // Reset after cooldown
      this.failureCount = 0;
    }

    try {
      const result = await fn();
      this.failureCount = 0; // Reset on success
      return result;
    } catch (error: any) {
      if (error.status === 429) {
        this.failureCount++;
        this.lastFailureTime = Date.now();
      }
      throw error;
    }
  }
}

Additional optimization tips

  • Monitor your usage: Track your API call patterns to identify optimization opportunities
  • Use webhooks: For real-time updates, consider using webhooks instead of polling endpoints
  • Optimize query patterns: Review your query logic to eliminate unnecessary or redundant API calls
  • Parallelize independent requests: Use Promise.all() for independent requests to reduce total execution time while staying within rate limits
By following these practices, your application can handle rate limits gracefully and provide a reliable experience for your users.