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

> Discover how GitHub Readme Stats caching works. Learn about TTL durations and HTTP headers that optimize API calls and speed up your stats cards by default 24 hour caching.

- Repository: [Anurag Hazra/github-readme-stats](https://github.com/anuraghazra/github-readme-stats)
- Tags: internals
- Published: 2026-02-28

---

**GitHub Readme Stats implements a server-side HTTP cache system in [`src/common/cache.js`](https://github.com/anuraghazra/github-readme-stats/blob/main/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`](https://github.com/anuraghazra/github-readme-stats/blob/main/src/common/cache.js):

```javascript
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:

```javascript
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`](https://github.com/anuraghazra/github-readme-stats/blob/main/src/common/cache.js) determines the final TTL through a strict precedence hierarchy:

```javascript
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`](https://github.com/anuraghazra/github-readme-stats/blob/main/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:

```javascript
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:

```javascript
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

```bash
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

```bash
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:

```bash
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`](https://github.com/anuraghazra/github-readme-stats/blob/main/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`](https://github.com/anuraghazra/github-readme-stats/blob/main/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`](https://github.com/anuraghazra/github-readme-stats/blob/main/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`](https://github.com/anuraghazra/github-readme-stats/blob/main/src/common/cache.js) handles this logic separately from standard successful responses.