Handling Rate Limits and CursorAgentError Exceptions in Production SDK Deployments
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 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 credentialsRateLimitError– HTTP 429 responses when exceeding request or usage quotasNetworkError– 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
isRetryableistrue, implement exponential backoff with jitter - If
isRetryableisfalse, 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 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 and error-handling documentation, follow these architectural patterns:
-
Centralized error handling – Wrap every
Agent.createandagent.sendblock in atry/catchthat inspectserr instanceof CursorAgentError. -
Respect
isRetryable– Implement exponential backoff with jitter using the formula2**attempt * 1000 + Math.random()*500and limit attempts to ≤ 3 for startup errors. -
Log essential identifiers – Capture
agent.agentIdafter creation,run.idafter send, andresult.status/result.durationMsafterwait(). These five fields let you correlate user-reported issues with specific runs in the Cursor dashboard. -
Do not exit on every error – A transient
RateLimitErrorshould trigger a back-off loop, not an immediateprocess.exit(1). Use exit code 75 (EX_TEMPFAIL) for retryable startup errors.
Code Implementation Examples
Minimal Retry Wrapper for Agent Creation
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
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
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– DefinesCursorAgentErrorhierarchy,isRetryablesemantics, and retry patterns.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– 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– Manages agent lifecycle, showing whereawait usingand proper disposal occur.cursor-sdk/README.md– High-level overview of the SDK and entry points (Agent.create,Agent.resume,Agent.prompt).
Summary
- Always check
err.isRetryablebefore 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, anddurationMsfor every operation to enable dashboard correlation. - Use exponential backoff with jitter (
2**attempt * 1000 + Math.random()*500) for transient failures likeRateLimitErrorandNetworkError. - 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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →