# Caching

> Accept a recent cached result with max_age and pay half the credits on a hit. How freshness, cache keys, X-Cache and meta.cached work.

By default every request fetches Google live. If a slightly older result is fine for your use case, set `max_age` to the oldest result you accept, in seconds. When SerpKite has a matching result that fresh, it returns it immediately for **half the credits**. Otherwise it fetches live at the normal price and stores the result for the next caller.

cURL:

```bash
curl https://api.serpkite.com/v1/search \
  -H "Authorization: Bearer $SERPKITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q":"best espresso machine 2026","country":"us","max_age":3600}'
```

TypeScript:

```ts
import { SerpKite } from "serpkite";

const sk = new SerpKite(); // reads SERPKITE_API_KEY
const res = await sk.search({ q: "best espresso machine 2026", country: "us", max_age: 3600 });
console.log(res.results[0].title, res.meta.credits_used);
```

Python:

```python
from serpkite import SerpKite

sk = SerpKite()  # reads SERPKITE_API_KEY
res = sk.search("best espresso machine 2026", country="us", max_age=3600)
print(res.results[0].title, res.meta.credits_used)
```

Go:

```go
package main

import (
	"context"
	"fmt"
	"log"

	serpkite "github.com/serpkite/serpkite-go"
)

func main() {
	ctx := context.Background()
	c := serpkite.NewClient() // reads SERPKITE_API_KEY
	res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us", MaxAge: 3600})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed)
}
```

## How it works

1. You send a request with `max_age: 3600` (one hour).
2. SerpKite looks up a result for the same normalised request: same endpoint, query, `country`, `language`, location, device, page, filters and so on.
3. If one exists and is younger than `max_age`, it is returned with `X-Cache: HIT` and costs 0.5× the usual credits.
4. If not, or if it is older, the search runs live at full price with `X-Cache: MISS`, and the fresh result replaces the cached one.

Without `max_age` (or with `max_age: 0`) you always get a live result and it is never served from cache. The maximum is 2,592,000 seconds (30 days).

## Pricing

A cache hit costs half of what the same live request would, rounded up to the next 0.001 credit so it is never free:

| Request | Live | Cache hit |
| --- | --- | --- |
| `/v1/search` (1 page) | 1 | 0.5 |
| `/v1/search` with `num: 100` | 7 | 3.5 |
| `/v1/autocomplete` | 0.5 | 0.25 |
| `/v1/lens` | 2 | 1 |

The `X-Credits-Used` header and `meta.credits_used` show the actual charge.

## Telling a hit from a miss

- The `X-Cache` response header is `HIT` or `MISS`.
- In JSON, `meta.cached` is `true` on a hit and `meta.cached_at` holds the time the result was fetched (ISO 8601).

```json
"meta": {
  "request_id": "req_01J8ZK4M6Q2V7",
  "credits_used": 0.5,
  "cached": true,
  "cached_at": "2026-09-29T07:12:44Z",
  "engine": "google",
  "parse_quality": "ok"
}
```

## How long results stay cached

`max_age` is the freshness you accept, but results also expire on their own. Current defaults:

| Endpoint | Kept for up to |
| --- | --- |
| Most endpoints | 6 hours |
| `/v1/news` | 30 minutes |
| `/v1/webpage` | 1 hour |
| `/v1/autocomplete` | 24 hours |

So `max_age: 86400` on `/v1/search` behaves like "anything from the last 6 hours". These retention times may change; `max_age` is the contract.

## Privacy

Every successful live result is written to the cache, whether or not the request sent `max_age`: `max_age` only decides whether *you* are served a cached copy. Entries are stored in memory under a SHA-256 hash of the normalised request, so the query text is never used as a key, and they expire automatically after the times above. Your identity and API key are not part of the entry. Raw HTML (`include_html`) is never cached. See [Privacy and data retention](https://serpkite.com/docs/privacy-and-data-retention).

## When to use it

- **Agents and chatbots:** popular questions repeat. `max_age: 3600` cuts cost on repeated queries without users noticing.
- **Dashboards that refresh often:** use a `max_age` a little shorter than your refresh interval.
- **Rank tracking:** usually leave it off, or set it to your tracking window, so each run reflects a fresh SERP. The [batch lane](https://serpkite.com/docs/batch) is the cheaper lever there.
- **News monitoring:** keep `max_age` short; news caching is capped at 30 minutes anyway.

`max_age` is also honoured inside [batch requests](https://serpkite.com/docs/batch) (`POST /v1/batches`).

Related: [Credits and billing](https://serpkite.com/docs/credits-and-billing), [Response headers](https://serpkite.com/docs/response-headers).