# Caching af adresseopslag

> Se hvilke svar du kan cache og hvor længe, hvad Cache-Control og ETag fra API'et betyder, og byg en cache i Python med og uden Redis.
>
> https://danadresse.dk/guides/caching · verified 2026-11-02
Adresser ændrer sig sjældent. En given adresse kan være uændret i årevis, og postnumre og kommuner skifter næsten aldrig. Derfor kan en stor del af dine kald besvares fra en cache i stedet for fra API'et. Det gør din side hurtigere og sparer af din kvote.

Guiden viser, hvad vi selv skriver i `Cache-Control`, hvordan ETag virker hos os, og to små cache-implementeringer.

## Hvad API'et sender

Hvert vellykket svar har en `Cache-Control`-header. Værdien afhænger af endpointet:

| Endpoint | `Cache-Control` |
|---|---|
| `/autocomplete` og `/<ressource>/autocomplete` | `private, max-age=300` |
| `/adresser` og `/adgangsadresser` (lister) | `private, max-age=300` |
| `/adresser/{id}` og `/adgangsadresser/{id}` | `private, max-age=3600` |
| `/adgangsadresser/reverse` | `private, max-age=60` |
| `/postnumre`, `/kommuner`, `/regioner`, `/sogne` og de andre DAGI-områder, `/vejstykker`, `/vejnavne`, `/navngivneveje`, `/jordstykker`, `/matrikelnumre`, `/ejerlav`, `/steder`, `/stednavne` | `private, max-age=86400, stale-while-revalidate=604800` |
| `/datavask/…` (GET) | `private, max-age=86400` |
| `/api/v1/enrich/…` | `private, max-age=86400` |
| Alle fejlsvar | `no-store` |

`private` betyder, at din egen klient må gemme svaret, men en delt cache undervejs (en CDN eller en fælles proxy) må ikke. Svaret hører til din nøgle. En browser, der henter `/postnumre`, genbruger altså svaret i et døgn uden at spørge igen.

Fejl må aldrig caches. Gemmer du et `429`- eller `503`-svar, bliver fejlen ved med at komme, efter at årsagen er væk.

## ETag og 304

JSON-svar op til 256 KB har en svag ETag, fx `ETag: W/"3f2a9c1b0d4e5f61"`. Sender du den tilbage i `If-None-Match`, og har svaret ikke ændret sig, får du `304 Not Modified` uden indhold:

```python id=etag-304 run=live expect="304"
import os

import requests

URL = "https://api.danadresse.dk/postnumre/8000"
headers = {"X-Api-Key": os.environ["DANADRESSE_API_KEY"]}

forste = requests.get(URL, headers=headers, timeout=10)
etag = forste.headers.get("ETag")
print("første kald:", forste.status_code, etag)

anden = requests.get(URL, headers={**headers, "If-None-Match": etag}, timeout=10)
print("andet kald: ", anden.status_code, len(anden.content), "bytes")
```

Et `304`-svar tæller stadig som et kald. Nøglen bliver kontrolleret og forbruget talt, før svaret bliver lavet. ETag sparer båndbredde og parsing, men ikke kvote. Vil du spare kald, skal svaret komme fra din egen cache uden at spørge API'et.

## Hvad du kan cache, og hvor længe

| Data | Forslag til levetid | Hvorfor |
|---|---|---|
| Autocomplete-forslag | Minutter, i hukommelsen | Brugere sletter og skriver igen. Den samme forespørgsel kommer tit flere gange i samme session |
| Opslag på id (`/adresser/{id}`) | Timer til et døgn | En adresse ændrer sig sjældent. Gem id'et i din database alligevel |
| Postnumre, kommuner, regioner, DAGI | Et døgn eller mere | Ændres højst få gange om året |
| Datavask af samme tekst | Et døgn | Samme input giver samme svar, indtil registret ændrer sig |
| Reverse-geokodning | Kort, eller pr. afrundet koordinat | GPS-punkter varierer, så samme nøgle rammes sjældent præcist |

Cache-nøglen skal indeholde stien og alle parametre, fx `srid`, `struktur` og `per_side`. Ellers får en kalder med `srid=25832` et svar i WGS84. Læg aldrig API-nøglen ind i cache-nøglen eller i logs.

Skal du bruge hele datasættet, så hent det med replikering i stedet for at cache dig frem til det med enkeltkald. Det står også i [vilkårene](/vilkaar), at hele datasættet skal hentes via replikeringen. Se [Første udtræk](/guides/replikering-foerste-udtraek).

## En TTL-cache i hukommelsen

Til en enkelt proces er en ordbog med udløbstid nok. Eksemplet slår det samme postnummer op to gange, men kalder kun API'et én gang:

```python id=ttl-cache run=live expect="API-kald: 1"
import os
import time

import requests

API = "https://api.danadresse.dk"
session = requests.Session()
session.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"]

_cache: dict[tuple, tuple[float, object]] = {}
api_kald = 0


def hent(sti: str, params: dict | None = None, ttl: float = 3600):
    global api_kald
    noegle = (sti, tuple(sorted((params or {}).items())))
    hit = _cache.get(noegle)
    if hit and hit[0] > time.monotonic():
        return hit[1]
    r = session.get(API + sti, params=params, timeout=10)
    r.raise_for_status()  # fejl caches ikke
    api_kald += 1
    data = r.json()
    _cache[noegle] = (time.monotonic() + ttl, data)
    return data


for _ in range(2):
    print(hent("/postnumre/8000", ttl=86400)["navn"])
print(f"API-kald: {api_kald}")
```

Cachen vokser uden grænse. Til autocomplete med mange forskellige forespørgsler bør du bruge en cache med en maksimal størrelse, fx `cachetools.TTLCache` i Python.

## En delt cache i Redis

Kører du flere processer eller servere, så læg cachen i Redis. Alle instanser deler så de samme svar. Levetiden vælges ud fra stien:

```python id=redis-cache run=syntax
import hashlib
import json
import os

import redis
import requests

API = "https://api.danadresse.dk"
r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379/0"))
session = requests.Session()
session.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"]

# Levetid i sekunder pr. sti-præfiks. Den første, der passer, vinder.
TTL = [
    ("/autocomplete", 300),
    ("/adgangsadresser/reverse", 60),
    ("/adresser/", 86400),
    ("/adgangsadresser/", 86400),
    ("/postnumre", 7 * 86400),
    ("/kommuner", 7 * 86400),
    ("/datavask/", 86400),
]


def ttl_for(sti: str) -> int:
    for praefiks, sekunder in TTL:
        if sti.startswith(praefiks):
            return sekunder
    return 300


def hent(sti: str, params: dict | None = None):
    raa = json.dumps([sti, sorted((params or {}).items())], ensure_ascii=False)
    noegle = "danadresse:" + hashlib.sha256(raa.encode()).hexdigest()
    gemt = r.get(noegle)
    if gemt is not None:
        return json.loads(gemt)
    svar = session.get(API + sti, params=params, timeout=10)
    svar.raise_for_status()  # kun 2xx caches
    data = svar.json()
    r.setex(noegle, ttl_for(sti), json.dumps(data, ensure_ascii=False))
    return data
```

Falder Redis ud, bør din kode kalde API'et direkte i stedet for at fejle. Pak `r.get` og `r.setex` ind i en `try`, hvis cachen ikke må kunne vælte forespørgslen.

## I browseren

Til et adressefelt er det nok at huske de sidste forespørgsler i et `Map`. Brugeren, der skriver "Vest", sletter og skriver "Vest" igen, får det andet svar uden et nyt kald:

```js id=browser-cache run=syntax
const cache = new Map();
const MAX = 100;

export async function forslag(q, apiKey) {
  if (cache.has(q)) return cache.get(q);
  const url = new URL("https://api.danadresse.dk/autocomplete");
  url.search = new URLSearchParams({ q, api_key: apiKey });
  const res = await fetch(url);
  if (!res.ok) return []; // fejl gemmes ikke
  const data = await res.json();
  cache.set(q, data);
  if (cache.size > MAX) cache.delete(cache.keys().next().value); // smid den ældste ud
  return data;
}
```

Nøglen her skal være en domænelåst browsernøgle. Se [Browsernøgler og tilladte domæner](/guides/browser-noegler-domaener).

## Næste skridt

- Hvad du gør, når et kald fejler: [Retries og backoff](/guides/retries-og-backoff).
- Se effekten på forbruget: [Overvåg forbruget](/guides/overvaag-forbrug).
