# Handling Rate Limits and CursorAgentError Exceptions in Production SDK Deployments

> Master Cursor SDK deployments by handling rate limits and CursorAgentError exceptions. Learn to use isRetryable for exponential backoff or permanent failure to ensure production stability.

- Repository: [Cursor/plugins](https://github.com/cursor/plugins)
- Tags: best-practices
- Published: 2026-05-25

---

**Production Cursor SDK deployments must distinguish between startup failures that throw `CursorAgentError` exceptions and run execution errors that return error statuses, using the `isRetryable` flag to determine whether to implement exponential backoff or fail permanently.**

Production deployments of the Cursor SDK require robust error handling to manage rate limits and agent failures gracefully. This guide demonstrates how to handle `CursorAgentError` exceptions and implement retry logic based on the SDK's error hierarchy as implemented in `cursor/plugins`. By understanding the distinction between transient startup errors and permanent execution failures, you can build resilient services that degrade gracefully rather than crashing on temporary rate limits.

## Understanding the CursorAgentError Hierarchy

The Cursor SDK provides a unified error hierarchy defined in [`cursor-sdk/skills/cursor-sdk/references/error-handling.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/error-handling.md) that encodes the HTTP/Connect code, a human-readable `message`, and a boolean **`isRetryable`** flag. This flag serves as the authoritative signal from the backend regarding whether an operation should be retried.

Key subclasses of `CursorAgentError` include:

- **`AuthenticationError`** – Invalid API keys or credentials
- **`RateLimitError`** – HTTP 429 responses when exceeding request or usage quotas
- **`NetworkError`** – Connection failures and transient network issues

Each error object carries metadata that allows your application to make intelligent decisions about retry logic without hardcoding HTTP status codes.

## Distinguishing Startup Failures from Run Execution Errors

The SDK exhibits two distinct failure modes that require separate handling strategies:

| Failure Mode | Trigger | SDK Surface | Handling Strategy |
|--------------|---------|-------------|-------------------|
| **Agent startup** | `Agent.create` or first `agent.send()` fails before run starts | Throws `CursorAgentError` subclass | Check `err.isRetryable`, implement backoff, retry if transient |
| **Run execution** | Prompt causes error during processing | `RunResult.status === "error"` (no exception thrown) | Log run ID, surface in dashboard, do not auto-retry |

**Startup failures** occur during initialization and always throw exceptions. These represent environmental issues like authentication problems, quota exhaustion, or network connectivity. According to the error-handling reference, you should catch these exceptions, inspect the `isRetryable` property, and implement backoff loops only for retryable errors.

**Run execution errors** happen when the agent successfully starts but fails to complete the task. These return status codes rather than throwing exceptions. Only retry when you know the failure is environmental (such as a flaky MCP server), not when the error stems from the prompt content itself.

## Implementing Rate Limit Handling

`RateLimitError` (HTTP 429) indicates you have exceeded your request or usage quota. The error object carries the **`isRetryable`** flag to distinguish between temporary throttling and permanent quota exhaustion:

- If `isRetryable` is `true`, implement exponential backoff with jitter
- If `isRetryable` is `false`, treat as permanent quota exhaustion and surface the problem to users

For bulk fan-out scenarios, the SDK's pattern guide in [`cursor-sdk/skills/cursor-sdk/references/patterns.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/patterns.md) recommends batching agents to avoid mass-hit limits. Process repositories or tasks in small batches rather than spawning hundreds of simultaneous agents.

## Production Error Handling Patterns

According to the source code in [`orchestrate/skills/orchestrate/scripts/core/agent-manager.ts`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/scripts/core/agent-manager.ts) and error-handling documentation, follow these architectural patterns:

1. **Centralized error handling** – Wrap every `Agent.create` and `agent.send` block in a `try/catch` that inspects `err instanceof CursorAgentError`.

2. **Respect `isRetryable`** – Implement exponential backoff with jitter using the formula `2**attempt * 1000 + Math.random()*500` and limit attempts to ≤ 3 for startup errors.

3. **Log essential identifiers** – Capture `agent.agentId` after creation, `run.id` after send, and `result.status`/`result.durationMs` after `wait()`. These five fields let you correlate user-reported issues with specific runs in the Cursor dashboard.

4. **Do not exit on every error** – A transient `RateLimitError` should trigger a back-off loop, not an immediate `process.exit(1)`. Use exit code 75 (`EX_TEMPFAIL`) for retryable startup errors.

## Code Implementation Examples

### Minimal Retry Wrapper for Agent Creation

```typescript
import { Agent, CursorAgentError, RateLimitError, NetworkError } from "@cursor/sdk";

async function createAgentWithRetry(opts: Parameters<typeof Agent.create>[0]) {
  const maxAttempts = 3;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await Agent.create(opts);
    } catch (err) {
      const retryable =
        err instanceof CursorAgentError &&
        err.isRetryable &&
        (err instanceof RateLimitError || err instanceof NetworkError);
      if (!retryable || attempt === maxAttempts) throw err;
      const backoff = 2 ** attempt * 1000 + Math.random() * 500;
      await new Promise(r => setTimeout(r, backoff));
    }
  }
  throw new Error("unreachable");
}

```

### GitHub Action with Proper Exit Codes

```typescript
import { Agent, CursorAgentError } from "@cursor/sdk";

async function main() {
  const {
    CURSOR_API_KEY,
    REPO_URL,
    HEAD_REF,
    BASE_REF,
    PR_URL,
  } = process.env;

  if (!CURSOR_API_KEY || !REPO_URL || !HEAD_REF) {
    console.error("Missing required env");
    process.exit(1);
  }

  await using agent = await createAgentWithRetry({
    apiKey: CURSOR_API_KEY,
    model: { id: "composer-2" },
    cloud: { repos: [{ url: REPO_URL, startingRef: HEAD_REF }], workOnCurrentBranch: true },
  });

  const prompt = `Review the changes on ${HEAD_REF} vs ${BASE_REF} for ${PR_URL}.`;
  try {
    const run = await agent.send(prompt);
    console.log(`[review] agent=${agent.agentId} run=${run.id}`);

    for await (const ev of run.stream()) {
      if (ev.type === "status") console.log(`[review] ${ev.status}`);
    }

    const result = await run.wait();
    if (result.status !== "finished") {
      console.error(`run ended ${result.status}`);
      process.exit(2);
    }
    console.log(`done ${result.durationMs}ms`);
  } catch (err) {
    if (err instanceof CursorAgentError) {
      console.error(`startup failed: ${err.message}`);
      process.exit(err.isRetryable ? 75 : 1); // 75 ⇒ EX_TEMPFAIL
    }
    throw err;
  }
}

```

### Fan-Out with Batch-Wise Rate-Limit Awareness

```typescript
import { Agent, CursorAgentError } from "@cursor/sdk";

async function dispatchOne(repo: string, apiKey: string, prompt: string) {
  await using agent = await createAgentWithRetry({
    apiKey,
    model: { id: "composer-2" },
    cloud: { repos: [{ url: repo, startingRef: "main" }], autoCreatePR: false },
  });
  try {
    const run = await agent.send(prompt);
    const result = await run.wait();
    return { repo, runId: result.id, status: result.status };
  } catch (err) {
    if (err instanceof CursorAgentError) {
      return { repo, error: err.constructor.name, message: err.message };
    }
    throw err;
  }
}

async function runInBatches<T, R>(items: T[], batchSize: number, fn: (t: T) => Promise<R>) {
  const out: R[] = [];
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    out.push(...(await Promise.all(batch.map(fn))));
  }
  return out;
}

```

## Key Source Files

- **[`cursor-sdk/skills/cursor-sdk/references/error-handling.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/error-handling.md)** – Defines `CursorAgentError` hierarchy, `isRetryable` semantics, and retry patterns.
- **[`cursor-sdk/skills/cursor-sdk/references/patterns.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/patterns.md)** – Provides production-ready integration templates that embed error-handling best practices.
- **[`orchestrate/skills/orchestrate/scripts/errors.ts`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/scripts/errors.ts)** – Runtime error definitions used by the orchestrator skill, illustrating how SDK error types surface in real implementations.
- **[`orchestrate/skills/orchestrate/scripts/core/agent-manager.ts`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/scripts/core/agent-manager.ts)** – Manages agent lifecycle, showing where `await using` and proper disposal occur.
- **[`cursor-sdk/README.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/README.md)** – High-level overview of the SDK and entry points (`Agent.create`, `Agent.resume`, `Agent.prompt`).

## Summary

- Always check **`err.isRetryable`** before implementing retry logic for startup errors.
- Distinguish between **startup exceptions** (thrown) and **run status errors** (return values) to avoid infinite retry loops on unrecoverable prompt failures.
- Log **`agentId`**, **`run.id`**, **`status`**, and **`durationMs`** for every operation to enable dashboard correlation.
- Use **exponential backoff with jitter** (`2**attempt * 1000 + Math.random()*500`) for transient failures like `RateLimitError` and `NetworkError`.
- Never call `process.exit(1)` on retryable errors; use exit code 75 or implement backoff loops to maintain service availability.

## Frequently Asked Questions

### What is the difference between CursorAgentError and RunResult.status errors?

`CursorAgentError` exceptions are thrown during agent startup (when calling `Agent.create` or the first `agent.send`), indicating environmental failures like authentication issues or rate limits. `RunResult.status` errors occur when an agent successfully starts but fails to complete the task, returning `status: "error"` without throwing an exception. You should retry startup errors when `isRetryable` is true, but generally should not retry run execution errors unless you know the failure is environmental.

### How do I know if a CursorAgentError is retryable?

Every `CursorAgentError` instance includes a boolean **`isRetryable`** property that serves as the authoritative signal from the backend. According to the error-handling reference in [`cursor-sdk/skills/cursor-sdk/references/error-handling.md`](https://github.com/cursor/plugins/blob/main/cursor-sdk/skills/cursor-sdk/references/error-handling.md), you should inspect this flag rather than parsing HTTP status codes or error messages. If `isRetryable` is `true`, implement exponential backoff and retry; if `false`, treat the error as permanent and surface it to users.

### What identifiers should I log for debugging Cursor SDK issues?

Log these five essential fields for every operation: **`agent.agentId`** after creation, **`run.id`** after calling `agent.send()`, and **`result.status`**, **`result.durationMs`**, and **`result.id`** after `run.wait()` completes. These identifiers allow you to correlate user-reported issues with specific runs in the Cursor dashboard and trace errors through distributed systems.

### How do I handle rate limits in batch processing scenarios?

For bulk operations, implement **batch-wise processing** rather than spawning all agents simultaneously. The SDK's patterns guide recommends limiting concurrent agents to avoid mass-hit limits. Use the `runInBatches` pattern shown above, and ensure each batch operation respects the `isRetryable` flag with exponential backoff. If `RateLimitError.isRetryable` is `false`, treat it as permanent quota exhaustion and halt the batch rather than continuing to retry.