paperclipai/paperclip · warning · RailwayError
railway_rate_limited
railway_rate_limited
Error message
Railway is rate limiting requests. Wait before trying again.
What it means
Thrown by query() (code railway_rate_limited) when Railway's API responds with HTTP 429, meaning too many requests in a window. It instructs the caller to wait before retrying rather than hammering the endpoint.
Solutions
- Back off and retry with exponential delay, honoring any Retry-After header
- Add caching/debouncing to status polling loops
- Serialize or rate-limit requests across all clients using the same token
- Reduce polling frequency (e.g. seconds to minutes) for deployment status
Example fix
// before
while (!done) status = await client.getDeployment(id); // 429
// after
await pRetry(async () => client.getDeployment(id), {
minTimeout: 1000, factor: 2, maxTimeout: 30000,
onFailedAttempt: e => { if (e.code !== "railway_rate_limited") throw e; }
}); Defensive patterns
Strategy: retry
Validate before calling
// client-side rate budget check before calls
class RailwayThrottle {
constructor(maxPerMin = 60) { this.times = []; this.max = maxPerMin; }
async gate() {
const now = Date.now();
this.times = this.times.filter(t => now - t < 60000);
if (this.times.length >= this.max) {
await new Promise(r => setTimeout(r, 60000 - (now - this.times[0])));
}
this.times.push(Date.now());
}
} Try / catch
try {
await client.query(document, variables);
} catch (e) {
if (e?.code === "railway_rate_limited") {
await sleep(expBackoff(attempt++, 1000, 30000));
return client.query(document, variables);
} else throw e;
} Prevention
- Poll deployment status with exponential backoff, not fixed tight loops
- Cache read results and share one client across concurrent callers
- Respect Retry-After headers when present
- Alert on repeated 429s indicating runaway automation
When it happens
Trigger: Polling deployment status in a tight loop; bulk project/service enumeration; multiple concurrent clients sharing one token; automated retries without backoff after a failure.
Common situations: Heartbeat-style pollers issuing GraphQL queries every few seconds; scripts iterating hundreds of environments; shared org token exhausted by several integrations.
Related errors
- dropping batch after attempt(s); event(s) lost
- quota
- quota
- Too many active setup-token login sessions.
- Anthropic Managed Agents request failed with HTTP
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/62bed6f238f1f12b.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/railway.ts:214
const secret = options.authorization.slice(7);
const redact = (value: unknown) => JSON.parse(redactSensitiveText(JSON.stringify(value).split(secret).join("[REDACTED]")));
async function query(document: string, variables: Record<string, unknown>): Promise<Record<string, any>> {
options.signal.throwIfAborted();
let response: Response;
try {
response = await options.request(RAILWAY_API_URL, { method: "POST", redirect: "error", signal: options.signal, headers: { "content-type": "application/json", Authorization: options.authorization }, body: JSON.stringify({ query: document, variables }) });
} catch (error) {
if (options.signal.aborted) throw options.signal.reason;
throw new RailwayError("railway_request_failed", "Railway could not be reached. A deployment request may have succeeded; inspect deployment status before retrying.");
}
if (response.status === 401 || response.status === 403) {
await response.body?.cancel();
throw new RailwayError("railway_api_authorization_required", "Railway rejected API access. Reconnect with access to the required workspace or project. Hosted connection tokens are used only if Railway accepts them for API access.", response.status);
}
if (!response.ok) {
await response.body?.cancel();
throw new RailwayError(response.status === 429 ? "railway_rate_limited" : "railway_api_unavailable", response.status === 429 ? "Railway is rate limiting requests. Wait before trying again." : "Railway is unavailable. Check deployment status before retrying a deployment operation.");
}
const body = await boundedResponseText(response, options.signal);
let payload: Record<string, any>;
try { payload = record(JSON.parse(body)); }
catch { throw new RailwayError("railway_invalid_response", "Railway returned an invalid API response."); }
if (payload.errors) {
// Provider errors can echo variables, credentials or application secrets.
if (Array.isArray(payload.errors) && payload.errors.some((error) => ["UNAUTHENTICATED", "FORBIDDEN"].includes(error?.extensions?.code) || ["Not Authorized", "Unauthorized", "Forbidden"].includes(error?.message))) {
throw new RailwayError("railway_api_authorization_required", "Railway denied this API request. Use IDs from a workspace selected during consent, or reconnect to grant access to the required workspace.", 403);
}
throw new RailwayError("railway_api_error", "Railway could not complete the request. Check target IDs, resource permissions, and deployment eligibility. Inspect status before retrying a mutation.");
}
if (!payload.data || typeof payload.data !== "object") throw new RailwayError("railway_invalid_response", "Railway returned no API data.");
return payload.data;
}
async function validateTarget(args: Record<string, any>) {
const data = await query(RAILWAY_QUERIES.target, { projectId: args.projectId, environmentId: args.environmentId, serviceId: args.serviceId });View on GitHub (pinned to 3f1d897a7c)