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

  1. Back off and retry with exponential delay, honoring any Retry-After header
  2. Add caching/debouncing to status polling loops
  3. Serialize or rate-limit requests across all clients using the same token
  4. 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

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


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)