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
- Concurrency limit: Process users in batches (e.g. 5 at a time) using a simple
pLimit-style utility or manual batching with sequential await
- 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
- 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
Summary
When the leaderboard cache expires,
buildLeaderboardfires up to 150 concurrent GitHub API requests in a single burst. This instantly exhausts the 5,000 req/hr GitHub API quota on the sharedGITHUB_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,buildLeaderboardfetches data for up to 50 users simultaneously with no concurrency limit: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
leaderboardCacheandipRateLimitsMap 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
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
pLimit-style utility or manual batching with sequentialawaitleaderboardCachefrom a module-level variable to Upstash Redis (already configured viametrics-cache.ts) so the cache survives across serverless instancesipRateLimitsto Redis too, for the same reason — the current in-memory Map resets on every cold start, defeating the rate limiterLabels
bugadvanced~6hPlease assign this to me under GSSoC