Skip to content

[BUG] [Bug] Leaderboard refresh fires up to 150 simultaneous GitHub API requests, exhausting the shared token and causing 403s for all users #696

@MehtabSandhu11

Description

@MehtabSandhu11

Summary

When the leaderboard cache expires, buildLeaderboard fires up to 150 concurrent GitHub API requests in a single burst. This instantly exhausts the 5,000 req/hr GitHub API quota on the shared GITHUB_TOKEN, causing subsequent API calls — including those from regular users on the dashboard — to fail with 403/429 errors for up to an hour.

Root Cause

In src/app/api/leaderboard/route.ts, buildLeaderboard fetches data for up to 50 users simultaneously with no concurrency limit:

const rows = await Promise.all(
  ((users ?? []) as PublicUser[]).map(async (user) => {
    const [monthlyCommits, streakCommits, prs] = await Promise.all([
      fetchCommitStats(user.github_login, monthStart),   // 1 API call
      fetchCommitStats(user.github_login, streakStart),  // 1 API call
      fetchPrCount(user.github_login, monthStart),       // 1 API call
    ]);
    ...
  })
);

With 50 users × 3 requests each = 150 simultaneous GitHub Search API requests. The GitHub Search API has a secondary rate limit of 30 requests/minute even with authentication. A burst of 150 will hit both the per-minute secondary limit and can dent the 5,000/hr primary limit significantly.

Additionally, the in-process leaderboardCache and ipRateLimits Map are module-level variables that are reset every time Vercel spins up a new serverless function instance. Under any meaningful traffic, multiple cold starts can each independently trigger a full 150-request burst simultaneously.

Impact

  • Dashboard metric API calls from real users fail with 502/GitHub API errors immediately after any leaderboard cache miss
  • On Vercel's serverless architecture, cache misses are more frequent than expected (new instances don't share memory)
  • The leaderboard page itself can time out since 150 sequential-in-parallel requests take time, and Vercel has a 10s serverless timeout on the free tier

Expected Behaviour

GitHub API requests during leaderboard builds should be rate-controlled. The leaderboard cache should survive across serverless instances (i.e. be stored in Redis/Upstash, which is already available in the project).

Proposed Fix

  1. Concurrency limit: Process users in batches (e.g. 5 at a time) using a simple pLimit-style utility or manual batching with sequential await
  2. Persistent cache: Move leaderboardCache from a module-level variable to Upstash Redis (already configured via metrics-cache.ts) so the cache survives across serverless instances
  3. Move ipRateLimits to Redis too, for the same reason — the current in-memory Map resets on every cold start, defeating the rate limiter

Labels

bug advanced ~6h

Please assign this to me under GSSoC

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions