Een cache versnelt een webapplicatie door eerder opgehaalde of berekende gegevens tijdelijk opnieuw te gebruiken. Je leest hoe caching in browsers, applicaties en databases werkt, welke gegevens daarvoor geschikt zijn en waarom verversing en consistentie bepalend zijn voor de uitkomst.
Wat caching in een webapplicatie doet en waar data wordt opgeslagen
Een cache bewaart gegevens die anders opnieuw moeten worden opgehaald of berekend. Dat kan een afbeelding zijn die de browser al eerder downloadde, een resultaat van een databasequery of een volledig samengestelde webpagina. Hergebruik vermindert wachttijd en belasting, maar introduceert ook een tweede versie van de gegevens. De applicatie moet bepalen wanneer die versie bruikbaar is en wanneer een nieuwe bron nodig is.
Caching gebeurt op verschillende plekken in de keten. Een browser bewaart responses op het apparaat van de bezoeker. Een Content Delivery Network (CDN) houdt kopieën dichter bij gebruikers vast. De applicatieserver kan berekende resultaten in geheugen of een gedeelde cache zoals Redis bewaren. Een database kan zelf queryresultaten, pagina’s of indexblokken cachen. Die lagen hebben elk hun eigen levensduur en regels; een wijziging in één laag maakt andere kopieën niet automatisch ongeldig.
Dat onderscheid is belangrijk bij het onderzoeken van afwijkende data. Een verouderde pagina kan bijvoorbeeld uit een CDN komen, terwijl de applicatie en database al de nieuwe waarde bevatten. Zonder zicht op de cachelaag lijkt het probleem willekeurig. Een bruikbaar ontwerp maakt daarom duidelijk welke laag een waarde levert, hoe lang die geldig is en welk mechanisme haar ververst of verwijdert.
Browsercache en HTTP-headers instellen voor webpagina’s
De browsercache werkt op basis van HTTP-responses. Met headers zoals Cache-Control, ETag en Last-Modified bepaalt een server of een response opnieuw kan worden gebruikt, eerst moet worden gecontroleerd of opnieuw moet worden opgehaald. Voor bestanden die nooit veranderen, zoals een versiegebonden JavaScriptbestand, is een lange cacheduur geschikt. Een gewijzigde versie krijgt dan een nieuwe bestandsnaam of hash. Zo hoeft een browser niet te raden of de inhoud veranderd is.
Voor HTML die gebruikersspecifieke of snel veranderende informatie bevat, gelden andere afwegingen. Een server kan bijvoorbeeld Cache-Control: no-cache gebruiken om hergebruik alleen na validatie toe te staan. Dat betekent niet hetzelfde als no-store, dat opslag moet voorkomen. Een verkeerde header kan ervoor zorgen dat een browser oude inhoud blijft tonen, of dat gevoelige informatie op een gedeeld apparaat wordt bewaard. Bij ingelogde pagina’s moet ook rekening worden gehouden met cookies en de cache-richtlijnen voor gedeelde proxies.
Een ETag laat de browser een eerder ontvangen versie identificeren. Bij een volgend verzoek kan de server met een kleine response melden dat die versie nog actueel is. Dit bespaart overdracht, maar werkt alleen goed als de server de validator betrouwbaar afleidt van de inhoud. Test headers daarom met echte browserverzoeken en controleer ook CDN-gedrag: een correct ingestelde browsercache compenseert geen verkeerd gecachete response aan de rand van het netwerk.
Applicatiecache voor berekende resultaten en veelgebruikte data
Een applicatiecache voorkomt dat een server telkens dezelfde dure bewerking uitvoert. Denk aan een overzicht met veel relaties, een externe API-aanroep of een berekening die voor dezelfde invoer steeds hetzelfde resultaat oplevert. De cache kan lokaal in het procesgeheugen staan of gedeeld worden via een dienst als Redis. Geheugencache is snel, maar verdwijnt bij een herstart en is niet vanzelf gelijk tussen meerdere applicatie-instanties. Een gedeelde cache geeft die instanties een gemeenschappelijke opslag, tegen extra netwerkverkeer en beheer.
Een cache-key moet alle invoer bevatten die het resultaat beïnvloedt. Bij een productoverzicht kunnen dat bijvoorbeeld taal, land, filters, gebruikersrol en catalogusversie zijn. Ontbreekt een relevante dimensie, dan kan een gebruiker een resultaat van een andere context krijgen. Te veel dimensies hebben het omgekeerde effect: de cache bevat veel bijna-identieke sleutels en de hergebruikratio daalt. Leg daarom vast welke parameters het antwoord inhoudelijk veranderen en normaliseer waarden zoals sortering en hoofdlettergebruik.
Ook capaciteit en foutgedrag horen bij het ontwerp. Een geheugenlimiet met een verwijderstrategie voorkomt dat een cache onbeheerst groeit, maar het verdwijnen van een item moet alleen extra werk veroorzaken, geen onjuist resultaat. Bij een storing van een gedeelde cache kan de applicatie tijdelijk rechtstreeks naar de bron terugvallen, mits die belasting aankan. Zonder begrenzing kan zo’n terugval juist de database overbelasten. Cachelogica vraagt dus om expliciete grenzen, niet alleen om een opslaglocatie.
Databasecaching en de invloed op queryprestaties
Databases gebruiken interne caches om veelgevraagde gegevens sneller beschikbaar te maken. Afhankelijk van het databasesysteem gaat het bijvoorbeeld om pagina’s uit tabellen en indexen, queryplannen of tussentijdse resultaten. Deze mechanismen zijn niet hetzelfde als een applicatiecache: de database kent de betekenis van een bedrijfswaarde vaak niet en kan daardoor niet bepalen dat een productoverzicht na een wijziging opnieuw moet worden opgebouwd. Interne caching vermindert schijfwerk, maar vervangt geen goede query, index of gegevensmodellering.
Een applicatie kan daarnaast queryresultaten buiten de database bewaren. Dat verlaagt het aantal databaseverzoeken, maar maakt consistentie expliciet een verantwoordelijkheid van de applicatie. Een querycache met sleutels op basis van SQL kan bijvoorbeeld weinig opleveren als query’s door dynamische waarden telkens anders zijn opgebouwd. Een cache op domeinniveau, zoals een product op ID, kan beter aansluiten bij de manier waarop wijzigingen worden verwerkt, maar vereist wel dat alle relevante mutaties die cache kennen.
Meet voordat je queryresultaten gaat cachen. Onderzoek uitvoerplannen, aantallen databaseverzoeken, queryduur en het aandeel herhaalde reads. Een trage query die telkens grote hoeveelheden data ophaalt, kan beter worden verbeterd met een index of aangepaste query dan verborgen achter een cache. Caching maakt de bronbelasting lager zolang de cache werkt; bij een lege cache, herstart of gelijktijdige verversing komt die belasting terug. De database moet die piek dus nog steeds verantwoord kunnen verwerken.
Welke gegevens geschikt zijn voor caching
Gegevens zijn vooral geschikt voor caching wanneer ze vaak worden gelezen, relatief duur zijn om te produceren en niet bij elke aanvraag veranderen. Een lijst met categorieën, een vertaling of een berekend dashboard kan bijvoorbeeld veel hergebruik opleveren. De afweging is niet alleen hoe vaak iets wijzigt, maar ook hoe schadelijk een tijdelijk oude waarde is. Een verouderde marketingtekst heeft doorgaans een andere impact dan een onjuiste voorraadstatus, accountmachtiging of actuele transactiepositie.
Voor elke cachebare waarde is een expliciet beleid nodig. Beschrijf de bron, de verwachte veranderfrequentie, de maximale toegestane ouderdom en de gevolgen van een cachemiss. Leg ook vast of de waarde per gebruiker verschilt. Persoonsgegevens en autorisatiebeslissingen vragen bijzondere aandacht: een gedeelde cache zonder correcte gebruikerscontext kan data tussen accounts lekken. Zelfs als de cache-key een account-ID bevat, moet de verwijdering bij intrekking van toegang betrouwbaar plaatsvinden.
Een bruikbare keuze volgt uit de eigenschappen van de data:
- Stabiele, openbare bestanden: lange caching werkt goed als wijzigingen een nieuwe URL opleveren.
- Veelgelezen referentiedata: een beperkte levensduur of expliciete invalidatie kan passend zijn.
- Gebruikersspecifieke resultaten: cache alleen met een volledige context in de sleutel en passende opslagregels.
- Snel veranderende kritieke waarden: haal ze rechtstreeks op of gebruik een mechanisme met expliciete versies en strakke consistentie-eisen.
De juiste keuze hangt af van de fout die acceptabel is. Caching is geen algemene optimalisatie die zonder domeinkennis kan worden aangezet.
Cache-invalidatie: resultaten verversen na een wijziging
Cache-invalidatie is het verwijderen of ongeldig verklaren van opgeslagen data zodra de bron verandert. De eenvoudige aanpak is een vaste time-to-live (TTL): na een ingestelde periode vervalt een item vanzelf. Dat beperkt hoe lang een resultaat verouderd kan blijven, maar geeft geen directe actualiteit. Een wijziging kan kort na het vullen plaatsvinden, waarna gebruikers tot het einde van de TTL de oude waarde zien. Een korte TTL verkleint dat venster, maar veroorzaakt meer cachemisses en bronverzoeken.
Bij expliciete invalidatie verwijdert de applicatie relevante sleutels na een succesvolle wijziging. Dat kan nauwkeurig zijn, maar de relatie tussen gewijzigde records en afgeleide resultaten is vaak ingewikkeld. Een productwijziging kan bijvoorbeeld ook zoekresultaten, categoriepagina’s en aanbevelingen beïnvloeden. Als één sleutel wordt vergeten, blijft een deel van de applicatie verouderd. Brede invalidatie verlaagt dat risico, maar wist mogelijk veel bruikbare data en veroorzaakt een piek in herberekeningen.
Versiebeheer biedt een alternatief voor het verwijderen van iedere variant. Een cache-key kan een catalogusversie of inhoudsversie bevatten. Na een wijziging wordt die versie verhoogd, zodat nieuwe verzoeken een andere sleutel gebruiken. Oude entries verdwijnen later door hun TTL of door capaciteitsbeheer. Dit maakt invalidatie voorspelbaarder, maar vereist een betrouwbare plek om de versie te lezen en consequent gedrag bij meerdere processen. Bij kritieke wijzigingen moeten opslag en versie-update bovendien zorgvuldig worden gecoördineerd: als de database commit slaagt maar de invalidatie faalt, kan een oude waarde langer beschikbaar blijven dan bedoeld.
Consistentie en verouderde data in meerdere cachelagen
Consistentie beschrijft hoe snel een wijziging zichtbaar wordt op alle plekken waar dezelfde gegevens worden gebruikt. In een systeem met database, applicatiecache en CDN kunnen die plekken verschillende versies bevatten. Een gebruiker schrijft een wijziging naar de database, maar krijgt vervolgens een waarde uit een oudere applicatiecache. Een andere gebruiker ziet misschien wel de nieuwe waarde, omdat diens verzoek een andere server of cachelaag raakt. Zulke verschillen zijn vaak tijdelijk, maar kunnen functionele fouten veroorzaken wanneer de software aanneemt dat alle antwoorden direct gelijk zijn.
Een veelgebruikte strategie is eventual consistency: wijzigingen verspreiden zich binnen een beperkte periode, zonder dat elk verzoek meteen de nieuwste waarde krijgt. Dat past bij inhoud waarbij een korte vertraging acceptabel is. Bij voorraad, saldo’s, toegangsrechten of workflowstatus kan de impact groter zijn. Daar kan een cache worden overgeslagen voor kritieke controles, of moet de applicatie een versie- of transactiemechanisme gebruiken waarmee zij oude data kan herkennen.
Lees-na-schrijven-gedrag verdient aparte aandacht. Na het opslaan van een wijziging verwacht de gebruiker vaak dat een daaropvolgende paginaweergave die wijziging toont. Dat lukt niet vanzelf wanneer de leesroute uit een cache antwoordt. De applicatie kan na een succesvolle mutatie de betrokken sleutel ongeldig maken, tijdelijk rechtstreeks uit de primaire database lezen of een nieuwe versie meesturen. Replicatie tussen databases kan nog steeds vertraging geven, dus een cache omzeilen is niet altijd voldoende. Ontwerp de zichtbare consistentie rond concrete gebruikershandelingen en test die met meerdere applicatie-instanties, niet alleen met één lokale server.
Cache stampedes, sleutelproblemen en andere fouten opsporen
Een cache stampede ontstaat wanneer een populair item verloopt en veel verzoeken tegelijk dezelfde dure berekening starten. De database of externe dienst krijgt dan in korte tijd een piekbelasting, precies wanneer de cache haar werk niet doet. Een lock per sleutel kan voorkomen dat iedere aanvraag opnieuw rekent: één proces vult de cache en andere verzoeken wachten kort of gebruiken tijdelijk de vorige waarde. De lock moet wel een beperkte geldigheidsduur hebben, zodat een vastgelopen proces de sleutel niet permanent blokkeert.
Een andere aanpak is verversen voordat een item verloopt. Background refresh kan een nieuwe waarde ophalen terwijl bestaande lezers nog de oude gebruiken. Dat beperkt gelijktijdige berekeningen, maar vraagt om grenzen voor hoe oud een fallback mag zijn. Willekeurige variatie op de TTL voorkomt dat veel sleutels tegelijk vervallen. Bij een externe dienst is bovendien een circuit breaker relevant: als de bron faalt, moet de applicatie niet onbeperkt nieuwe pogingen starten en de storing verergeren.
Cacheproblemen zijn lastig te diagnosticeren als alleen de totale responstijd wordt gemeten. Registreer daarom cachehits, misses, verversingen, fouten en de ouderdom van teruggegeven waarden. Vermijd cachekeys en logregels met persoonsgegevens. Controleer ook cardinaliteit: een onverwacht groot aantal unieke sleutels wijst vaak op een ontbrekende normalisatie of een dimensie die onbedoeld in de sleutel terechtkomt. Bij een foutmelding is het nuttig te onderscheiden of de cache onbereikbaar is, een item ontbreekt of de bron zelf faalt. Die signalen bepalen of de juiste reactie herstel, terugval of het bewust weigeren van een verouderd resultaat is.
Veelgestelde vragen
Hoe vind ik uit welke cachelaag een verouderde pagina serveert?
Vergelijk de response op verschillende punten in de aanvraagketen en controleer welke laag als eerste de oude inhoud teruggeeft. Leg daarvoor de relevante responseheaders vast, zoals Age, ETag en Cache-Control, en voeg waar mogelijk een header of logveld toe dat de serverinstantie en cachebron aanduidt. Vraag dezelfde URL vervolgens rechtstreeks bij de applicatieserver op en vergelijk die response met de openbare URL. Zo kun je zien of de afwijking bij de browser, het CDN of de applicatiecache ontstaat.
Herhaal de controle met een unieke queryparameter of een verzoek dat de cache omzeilt, als je infrastructuur dat veilig ondersteunt. Gebruik zulke controles niet als structurele oplossing: ze zijn bedoeld om de bron van het probleem te isoleren.
Welke cachemetrics moet ik monitoren in een webapplicatie?
Monitor in elk geval het cache-hitpercentage, het aantal missers, de responstijd van cacheverzoeken en de belasting van de achterliggende database of API. Die combinatie laat zien of de cache daadwerkelijk werk bespaart en of missers tot merkbare vertraging of extra bronbelasting leiden. Meet daarnaast het aantal time-outs en fouten, het geheugengebruik en het aantal verwijderde of verlopen items.
Bekijk cijfers per cachelaag en, waar nuttig, per type sleutel of endpoint; één totaalpercentage kan problemen bij een belangrijk onderdeel verbergen. Stel waarschuwingen in voor plotselinge dalingen in hits, oplopende latency of een gelijktijdige piek in databaseverkeer. Gebruik vaste dashboards en vergelijk gedrag tijdens normale belasting met deployments en piekmomenten.
Hoe voorkom ik dat cache-invalidatie verloren gaat na een databasewijziging?
Gebruik een transactionele outbox om de wijziging en het bericht voor cache-invalidatie samen betrouwbaar vast te leggen. De applicatie schrijft de databasewijziging en een invalidatiegebeurtenis in dezelfde transactie; een achtergrondproces verstuurt die gebeurtenis daarna opnieuw totdat verwerking is bevestigd. Zo kan een tijdelijke storing tussen de database en de cache de invalidatie niet stilzwijgend laten verdwijnen.
Maak de verwerking idempotent, zodat hetzelfde bericht veilig meerdere keren mag aankomen. Registreer mislukte pogingen en geef berichten die herhaaldelijk falen een plek voor nader onderzoek. Bij kritieke gegevens kan een korte TTL als extra vangnet dienen, maar die vervangt geen betrouwbare verwerking van wijzigingen.
Hoe voorkom ik een cache stampede wanneer een populair item verloopt?
Voorkom een cache stampede door per sleutel slechts één aanvraag tegelijk het verlopen item opnieuw te laten berekenen. Andere aanvragen kunnen kort wachten, een tijdelijke oude waarde ontvangen of een gecontroleerde fout terugkrijgen als wachten niet acceptabel is. Gebruik een lock met een maximale duur en zorg dat die ook vrijkomt wanneer het proces dat de cache vult vastloopt.
Je kunt daarnaast vervaltijden licht spreiden, zodat veel items niet op hetzelfde moment verlopen, en populaire sleutels vooraf vernieuwen. Bij een aanpak met tijdelijk verouderde antwoorden moet de applicatie bepalen voor welke gegevens dat veilig is. Begrens ook het aantal gelijktijdige berekeningen, zodat een storing niet alsnog de database overbelast.
Hoe test ik of caching correct blijft werken bij gelijktijdige verzoeken?
Test caching met meerdere gelijktijdige verzoeken die dezelfde waarde opvragen terwijl die waarde verloopt of wordt gewijzigd. Controleer of slechts een beheersbaar aantal aanvragen de dure berekening uitvoert, of alle gebruikers uiteindelijk de nieuwe versie zien en of fouten tijdens het verversen geen onjuiste gegevens opslaan. Voer de test uit met meerdere applicatie-instanties als productie die ook gebruikt.
Neem scenario’s op voor een koude cache, een cachestoring, een mislukte invalidatie en een wijziging die direct wordt gevolgd door een leesverzoek. Controleer niet alleen de uiteindelijke response, maar ook databasebelasting, logs en cache-inhoud. Herhaal de tests onder piekbelasting en leg vast welke tijdelijke afwijkingen voor de betreffende gegevens acceptabel zijn.