How GitHub Readme Stats Caching Works: TTL Durations and HTTP Headers

GitHub Readme Stats implements a server-side HTTP cache system in src/common/cache.js that reduces GitHub GraphQL API calls by setting Cache-Control headers with configurable TTL values, defaulting to 24 hours for stats cards while enforcing per-card minimum and maximum bounds.

GitHub Readme Stats generates dynamic SVG cards for developer profiles, but frequent requests to the GitHub API risk hitting rate limits. The project solves this through a configurable caching layer that controls exactly how long responses remain valid in browsers and edge CDNs.

Cache Duration Constants

The canonical time spans are defined as seconds in the DURATIONS object within src/common/cache.js:

const DURATIONS = {
  ONE_MINUTE: 60,
  FIVE_MINUTES: 5 * 60,
  TEN_MINUTES: 10 * 60,
  FIFTEEN_MINUTES: 15 * 60,
  THIRTY_MINUTES: 30 * 60,

  TWO_HOURS: 2 * 60 * 60,
  FOUR_HOURS: 4 * 60 * 60,
  SIX_HOURS: 6 * 60 * 60,
  EIGHT_HOURS: 8 * 60 * 60,
  TWELVE_HOURS: 12 * 60 * 60,

  ONE_DAY: 24 * 60 * 60,
  TWO_DAY: 2 * 24 * 60 * 60,
  SIX_DAY: 6 * 24 * 60 * 60,
  TEN_DAY: 10 * 24 * 60 * 60,
};

These constants provide the baseline for all cache calculations across the application.

Per-Card TTL Configuration

Each card type maintains its own CACHE_TTL profile with default, minimum, and maximum values to balance freshness against API load:

const CACHE_TTL = {
  STATS_CARD: {
    DEFAULT: DURATIONS.ONE_DAY,      // 86,400 seconds
    MIN: DURATIONS.TWELVE_HOURS,     // 43,200 seconds
    MAX: DURATIONS.TWO_DAY,          // 172,800 seconds
  },
  TOP_LANGS_CARD: {
    DEFAULT: DURATIONS.SIX_DAY,
    MIN: DURATIONS.TWO_DAY,
    MAX: DURATIONS.TEN_DAY,
  },
  PIN_CARD: {
    DEFAULT: DURATIONS.TEN_DAY,
    MIN: DURATIONS.ONE_DAY,
    MAX: DURATIONS.TEN_DAY,
  },
  GIST_CARD: {
    DEFAULT: DURATIONS.TWO_DAY,
    MIN: DURATIONS.ONE_DAY,
    MAX: DURATIONS.TEN_DAY,
  },
  WAKATIME_CARD: {
    DEFAULT: DURATIONS.ONE_DAY,
    MIN: DURATIONS.TWELVE_HOURS,
    MAX: DURATIONS.TWO_DAY,
  },
  ERROR: DURATIONS.TEN_MINUTES,      // 600 seconds
};

The Top Languages card uses the longest default cache (6 days) because language statistics change infrequently, while error responses are cached for only 10 minutes to ensure quick recovery from transient failures.

Cache Resolution Logic

The resolveCacheSeconds function in src/common/cache.js determines the final TTL through a strict precedence hierarchy:

const resolveCacheSeconds = ({ requested, def, min, max }) => {
  let cacheSeconds = clampValue(isNaN(requested) ? def : requested, min, max);

  if (process.env.CACHE_SECONDS) {
    const envCacheSeconds = parseInt(process.env.CACHE_SECONDS, 10);
    if (!isNaN(envCacheSeconds)) {
      cacheSeconds = envCacheSeconds;
    }
  }

  return cacheSeconds;
};

The logic follows this priority:

  1. Environment variable: CACHE_SECONDS overrides all other values if set
  2. Query parameter: ?cache_seconds= value clamped between min/max
  3. Default: Card-specific default from CACHE_TTL

The clampValue utility (imported from src/common/ops.js) enforces boundaries, ensuring users cannot specify absurdly short caches (below minimum) or excessively long ones (above maximum).

HTTP Header Implementation

Once resolved, the setCacheHeaders function applies standard HTTP caching directives:

const setCacheHeaders = (res, cacheSeconds) => {
  if (cacheSeconds < 1 || process.env.NODE_ENV === "development") {
    disableCaching(res);
    return;
  }

  res.setHeader(
    "Cache-Control",
    `max-age=${cacheSeconds}, ` +
      `s-maxage=${cacheSeconds}, ` +
      `stale-while-revalidate=${DURATIONS.ONE_DAY}`,
  );
};

This generates a Vercel-compatible Cache-Control header with three directives:

  • max-age: Browser private cache duration
  • s-maxage: Shared CDN cache duration
  • stale-while-revalidate: Serves stale content for up to 24 hours while fetching fresh data in the background

Error Response Caching

Failed requests trigger setErrorCacheHeaders, which applies the short 10-minute error TTL unless caching is globally disabled:

const setErrorCacheHeaders = (res) => {
  const envCacheSeconds = process.env.CACHE_SECONDS
    ? parseInt(process.env.CACHE_SECONDS, 10)
    : NaN;

  if ((!isNaN(envCacheSeconds) && envCacheSeconds < 1) ||
      process.env.NODE_ENV === "development") {
    disableCaching(res);
    return;
  }

  res.setHeader(
    "Cache-Control",
    `max-age=${CACHE_TTL.ERROR}, ` +
      `s-maxage=${CACHE_TTL.ERROR}, ` +
      `stale-while-revalidate=${DURATIONS.ONE_DAY}`,
  );
};

This ensures broken cards recover quickly once the underlying issue resolves.

Configuration Methods

Mechanism Implementation Effect
Query Parameter ?cache_seconds=43200 Request-specific TTL clamped to card min/max
Environment Variable CACHE_SECONDS=300 Global override bypassing all min/max limits
Development Mode NODE_ENV=development Disables caching entirely via disableCaching()
Zero Value cache_seconds=0 Forces no-cache, no-store, must-revalidate headers

Practical Examples

Request a 12-hour cache for a stats card

curl -I "https://github-readme-stats.vercel.app/api?username=anuraghazra&cache_seconds=43200"

Response headers:


Cache-Control: max-age=43200, s-maxage=43200, stale-while-revalidate=86400

Disable caching for a single request

curl -I "https://github-readme-stats.vercel.app/api?username=anuraghazra&cache_seconds=0"

Response headers:


Cache-Control: no-cache, no-store, must-revalidate, max-age=0, s-maxage=0
Pragma: no-cache
Expires: 0

Force 5-minute cache globally via Vercel

Set the environment variable in your Vercel project dashboard:

CACHE_SECONDS=300

All endpoints will now emit Cache-Control: max-age=300 regardless of card-specific defaults.

Summary

  • GitHub Readme Stats uses HTTP Cache-Control headers implemented in src/common/cache.js to minimize GitHub GraphQL API consumption
  • Default durations vary by card type: 24 hours for stats cards, 6 days for top languages, and 10 days for pinned repositories
  • Bounds enforcement via resolveCacheSeconds prevents cache values below minimums (typically 12 hours) or above maximums (typically 2-10 days)
  • Global override via the CACHE_SECONDS environment variable takes precedence over all query parameters and card defaults
  • Error resilience through 10-minute error caching ensures temporary failures don't persist indefinitely
  • Development mode automatically disables caching when NODE_ENV equals development

Frequently Asked Questions

How long does GitHub Readme Stats cache responses by default?

Default cache durations vary by card type: the stats card caches for 24 hours (DURATIONS.ONE_DAY), the top languages card caches for 6 days (DURATIONS.SIX_DAY), and pinned repository cards cache for 10 days (DURATIONS.TEN_DAY). These values are defined in the CACHE_TTL constant within src/common/cache.js.

Can I completely disable caching in GitHub Readme Stats?

Yes. Set the cache_seconds query parameter to 0 for a single request, or set the CACHE_SECONDS environment variable to 0 for a global override. Alternatively, running the application with NODE_ENV=development automatically disables caching by invoking the disableCaching() function, which sets no-cache, no-store headers.

Why is there a maximum cache limit for each card?

The maximum limits protect the service from stale data and ensure that profile statistics remain reasonably current. For example, the stats card enforces a maximum of 2 days (DURATIONS.TWO_DAY) even if a user requests a longer duration via ?cache_seconds=. The clampValue function in src/common/ops.js enforces these boundaries during the resolveCacheSeconds calculation.

How does error caching differ from successful response caching?

Error responses use a fixed 10-minute TTL (CACHE_TTL.ERROR or DURATIONS.TEN_MINUTES) regardless of the card type or requested cache duration. This short window ensures that transient GitHub API failures or rate limits resolve quickly without serving broken SVG cards for extended periods. The setErrorCacheHeaders function in src/common/cache.js handles this logic separately from standard successful responses.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →