Skip to content
Site updates

GGScore API Rate Limits, Caching, and Free Tier: A Production Guide

6/25/2026, 12:37:12 PM

Hit 429 on launch day? Free tier is 3 req/day—enough to prototype, not to poll from the browser. Cache server-side, budget requests, upgrade when logs say so.

GGScore API Rate Limits, Caching, and Free Tier: A Production Guide

Your CS2 tracker works in dev. You ship to ten users. On day one the API returns 429 Too Many Requests and your dashboard goes blank. The bug is rarely "bad code" — it is uncached client-side polling against a daily quota you never budgeted.

GGScore is a paid CS2 match data API with clear tiers: Free for prototyping, Premium and Black for real traffic. This guide shows how to count requests, cache server-side, and handle 429 so Free stays useful and production stays stable. Verify current limits on ggscore.net pricing before you ship.


What You Are Actually Limited On

Public tiers (check live page for exact numbers):

TierOrient. quotaTypical use
Free3 requests / dayKey test, one cron, local integration
Premium100 requests / daySmall bot or widget with cache
BlackUnlimited positioningProduction scale, priority support

Free also allows multiple API keys (e.g. 5 keys on the public site) — useful for dev/staging separation, not for multiplying the same daily quota across keys in one app.

Important: limits are usually per account / key policy, not "unlimited if I open five tabs." Design as if the whole product shares one budget until docs say otherwise.


Why 3 Requests Per Day Is Enough (and Not Enough)

Enough for:

  • One played-matches call to inspect JSON shape
  • One upcoming-matches call to map schedule fields
  • One spare call after you fix a mapping bug

Not enough for:

  • Calling the API on every page view from the browser
  • A Discord bot polling every 30 seconds
  • Health checks that hit GGScore every minute
  • Retries without backoff burning the daily cap in minutes

Free is a contract test, not hosting for users.


Request Budget: Count Before You Code

List every caller that touches GGScore:

SourceEndpointCalls per user actionUsers / hourUncached daily risk
Web homepageplayed + upcoming2500 visitors1000+
Discord /recentplayed1200 commands200+
Cron refreshboth224 runs48

With 3/day on Free, even a single uncached homepage widget exceeds quota before lunch.

Rule: every production path goes through your backend + cache. The browser never holds the API key; it never calls GGScore directly.


Server-Side Cache: Non-Negotiable

Pattern

[Browser / Bot / Mobile]
        ↓
[Your API route]  ← reads cache first
        ↓
[GGScore]         ← only on cache miss (or TTL expiry)

Suggested TTLs (starting points)

DataEndpoint familyTTLWhy
Recent resultsplayed-matches60–120 sScores change slowly after match end
Today's scheduleupcoming-matches5–15 minFixtures shift less often than live scores
Countries / static refscountries (per docs)24 hRarely changes

Tune TTLs against your tier: on Premium (100/day), a 10-minute upcoming cache = max ~144 calls/day per endpoint if you poll naively — still too many. Cache per cache key, not per visitor.

Single-flight (dedupe thundering herd)

When cache expires, 50 simultaneous users should not trigger 50 upstream calls:

const inflight = new Map();

async function getPlayedCached(redis, fetchPlayed) {
  const key = 'ggscore:played:limit10';
  const hit = await redis.get(key);
  if (hit) return JSON.parse(hit);

  if (!inflight.has(key)) {
    inflight.set(key, fetchPlayed().finally(() => inflight.delete(key)));
  }
  const data = await inflight.get(key);
  await redis.setex(key, 90, JSON.stringify(data));
  return data;
}

Same idea in Python with asyncio.Lock or a short-lived "loading" flag in Redis.

Stale-while-revalidate (optional)

On 429 or upstream timeout, serve stale cache with a stale: true flag in your API response instead of an empty UI. Log the incident; alert if stale age > 30 minutes during a tournament day.


Handling 401 and 429

StatusMeaningAction
401Missing / invalid X-API-KeyFix env, rotate leaked key, never expose in frontend
429Quota exceededBack off; do not retry in a tight loop

Backoff sketch (Node)

async function fetchWithBackoff(url, options, maxAttempts = 4) {
  let delayMs = 1000;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;
    if (attempt === maxAttempts) throw new Error('Rate limited after retries');
    await new Promise((r) => setTimeout(r, delayMs));
    delayMs *= 2;
  }
}

On Free, treat the first 429 of the day as "stop until tomorrow" for automated jobs — do not burn retries that will never succeed.

User-facing copy: "Match data temporarily unavailable — cached results shown" beats a silent failure.


Example: One Day on Free vs Premium

Scenario: landing page shows last 5 played + next 5 upcoming.

StrategyGGScore calls / visitor100 visitors/day
No cache, client-side2200
Server cache 10 min2 / 1440 min ≈ 0.003 avg~0.3 effective bursts per 10 min window
Server cache + single-flight2 per TTL window2–4 per endpoint per day

Free (3/day): only the cached server pattern works for a public page — and only if traffic is low. Real launch → Premium or Black.


Discord and Cron: Poll Smarter

From the Discord bot guide:

  • One subcommand = one cached backend route, not one live GGScore call per click.
  • Hash last payload; skip Discord message edits when nothing changed.
  • Cron every 5–15 minutes for upcoming, not every 30 seconds.

If you need both played and upcoming, see played + upcoming fundamentals for endpoint split — two cache keys, two TTLs, one shared daily budget. (Fix slug to cs2-played-upcoming-matches-api-guide in admin when editing that post.)


When to Upgrade

SignalLikely tier
Solo dev, local tests onlyFree
Small server / blog widget, cache in place, <100 upstream calls/day measuredPremium
Public product, many users, tight freshness, or burst trafficBlack

Upgrade when measured daily upstream calls (from logs) exceed ~80% of quota for three days — not on day one optimism.


Logging and Alerts (Minimal)

Log per upstream call:

  • endpoint, status, latency, cache hit/miss
  • remaining header if docs expose it (do not assume)

Alert when:

  • 429 rate > 0 in production
  • cache miss storm (>10 upstream calls in 1 minute)
  • stale responses served > 15 minutes during an event you advertise as "live"

Security Reminder

  • API key in server env only (GGSCORE_API_KEY)
  • Rotate on leak; separate keys for staging if policy allows
  • Never commit keys to Git or embed in client bundles

Checklist Before Go-Live

  • Request budget table written (who calls what, how often)
  • All user traffic hits your API, not GGScore directly
  • TTLs set per endpoint (played shorter than upcoming)
  • Single-flight or lock on cache miss
  • 429 backoff + user-visible fallback (stale cache OK)
  • Daily upstream call count measured in staging
  • Tier matches budget (Free ≠ production)
  • Pricing page re-checked for current req/day numbers

CTA

  • Get an API key at ggscore.net — validate on Free, measure, then upgrade.
  • Read the docs for exact auth headers, endpoints, and response schemas.
  • Build cache first, features second — your future self at 429 o'clock will thank you.

Bottom Line

Rate limits are a design input, not a surprise tax. GGScore Free proves the integration; caching and the right tier make it a product. Count requests, cache on the server, back off on 429, upgrade when logs say so.

What is your current cache TTL for upcoming matches — still polling every page load?

GGScore API Rate Limits, Caching, and Free Tier: A Production Guide | GGScore