---
title: Caching
description: How Gitupcms keeps GitHub calls low without a content database, and what happens when GitHub's request limit is reached.
---

Gitupcms has no content database. Your repository on GitHub is the only copy of your content, so there is nothing to sync and nothing to go stale. The caches below only save GitHub requests; each one is keyed so it can never serve old content.

## What is cached

| Layer | Where | Keyed by | Goes stale? |
|---|---|---|---|
| File tree | — | One recursive tree call per request | Never: read fresh each time |
| File text | SQLite `blob_cache` | Git blob sha | Never: a changed file has a new sha |
| Detected schema | Memory | Tree sha | Never: a changed repo has a new tree sha |
| Media dates | SQLite `media_date` | Blob sha | Never |
| GitHub GET answers | Memory, 500 entries per server | Token + URL, with the ETag | Never: GitHub confirms each one |

### File tree and file text

Each page load reads the branch's whole file tree in one call. File contents are then read in batches through GraphQL, and only for blobs that are not cached yet. Because a blob sha is the hash of the file's content, an edited file gets a new sha and is read again; an unchanged file is never read twice.

### Conditional requests (ETag)

Every GitHub GET is sent with the ETag of its last answer. When nothing changed, GitHub answers `304 Not Modified`, and **a 304 does not count against the rate limit**. Gitupcms then reuses the answer it already has.

- Answers are stored per token (as a hash, never the token itself), because private repositories and permissions differ per user.
- The cache lives in memory and holds the 500 most recent answers. A restart empties it; the first requests after a restart cost the rate limit again.

## When the rate limit is reached

GitHub allows 5,000 requests an hour per user. If a user reaches it, the CMS shows *"GitHub's request limit was reached. Try again after HH:MM UTC."* with the time the limit resets, instead of a generic error.

## What is not cached, on purpose

- **No time-based cache (TTL).** A cached answer that expires after N minutes can show content that was changed on GitHub a minute ago. Every cache above is checked against GitHub or keyed by content, so a commit made outside the CMS shows up on the next page load.
- **No webhooks.** Nothing needs to be invalidated, so there is no GitHub App or webhook to set up.

The SQLite database (`DB_PATH`, WAL mode) holds accounts, workspaces, settings and comments, but never page content. Clearing the `blob_cache` and `media_date` tables is safe: they refill on their own.
