overleaf/overleaf · warning · CodedError

TooManyRequests

TooManyRequests

Error message

rate-limit hit when joining project

What it means

joinProject proxies a join-project request to the web API; when that upstream call answers HTTP 429 the manager rethrows it as a CodedError with code 'TooManyRequests'. It signals the server-side rate limiter rejected the join attempt, not a bug in the caller's payload. Retry only after honoring the backoff interval.

Source

Thrown at services/real-time/app/js/WebApiManager.js:37

  const url = new URL(settings.apis.web.url)
  url.pathname = Path.posix.join('project', projectId, 'join')
  let data
  try {
    data = await fetchJson(url, {
      method: 'POST',
      basicAuth: {
        user: settings.apis.web.user,
        password: settings.apis.web.pass,
      },
      json: {
        userId,
        anonymousAccessToken: user.anonymousAccessToken,
      },
    })
  } catch (error) {
    if (error instanceof RequestFailedError) {
      if (error.response.status === 429) {
        throw new CodedError(
          'rate-limit hit when joining project',
          'TooManyRequests'
        )
      } else if (error.response.status === 403) {
        throw new NotAuthorizedError()
      } else if (error.response.status === 404) {
        throw new CodedError('project not found', 'ProjectNotFound')
      }
      throw new WebApiRequestFailedError(error.response.status)
    }
    throw OError.tag(error, 'join project request failed')
  }
  if (!(data && data.project)) {
    throw new CorruptedJoinProjectResponseError()
  }
  const userMetadata = {
    isRestrictedUser: data.isRestrictedUser,
    isTokenMember: data.isTokenMember,

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. Wait and retry with exponential backoff, honoring any Retry-After header from the response
  2. Cache the join result so repeated navigations/rejoins don't re-issue the request
  3. Add jittered client-side throttling so concurrent tabs don't each call joinProject
  4. Ask the admin whether the rate-limit threshold should be raised for your workload

Example fix

// before
for (const p of projects) await realTimeClient.joinProject(userId, p)
// after
for (const p of projects) {
  await withBackoff(() => realTimeClient.joinProject(userId, p)) // retries on TooManyRequests with delay
}
Defensive patterns

Strategy: retry

Validate before calling

// client-side token bucket before calling joinProject
if (!limiter.tryAcquire('joinProject', { capacity: 5, refillPerMin: 10 })) {
  await sleep(backoff())
}

Try / catch

try {
  await manager.joinProject(user, projectId)
} catch (err) {
  if (err instanceof CodedError && err.code === 'TooManyRequests') {
    await sleep(jitteredBackoff(attempt))
    return retry()
  }
  throw err
}

Prevention

When it happens

Trigger: Calling joinProject when the upstream web API returns status 429 — i.e., the user/project has exceeded the allowed rate of join-project requests (rapid re-joins, scripted loops, or many concurrent tab joins).

Common situations: Scripts or tests hammering joinProject in a loop; a user opening the same project in many tabs simultaneously; aggressive reconnection logic retrying immediately after failures without backoff.

Related errors


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/80a91b47870caa21. Report an issue: GitHub.