[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-post-en-\u002Fblog\u002Frijmwoordenboek-caching-optimization-\u002Fen\u002Fblog\u002Frijmwoordenboek-caching-optimization":3,"blog-post-surround-en-\u002Fblog\u002Frijmwoordenboek-caching-optimization-\u002Fen\u002Fblog\u002Frijmwoordenboek-caching-optimization":1497,"related-posts-en-\u002Fblog\u002Frijmwoordenboek-caching-optimization-\u002Fen\u002Fblog\u002Frijmwoordenboek-caching-optimization":1504},{"id":4,"title":5,"authors":6,"badge":13,"body":15,"categories":1448,"date":1450,"description":1451,"extension":1452,"image":1453,"meta":1455,"navigation":403,"path":1486,"readingTime":207,"seo":1487,"stem":1488,"tags":1489,"__hash__":1496},"posts_en\u002Fblog\u002F7.rijmwoordenboek-caching-optimization.md","Rijmwoordenboek: Serving Pages Under 15ms with Better Caching",[7],{"name":8,"to":9,"avatar":10,"bio":12},"Rob Schoenaker","https:\u002F\u002Flinkedin.com\u002Fin\u002Frobschoenaker",{"src":11},"\u002Fimages\u002Fteam\u002Frob.jpg","Managing Partner at UpstreamAds and Partner at Ludulicious B.V. with over 20 years of experience in software development, specializing in .NET Core, ServiceStack, C# and database design.",{"label":14},"Caching",{"type":16,"value":17,"toc":1421},"minimark",[18,23,27,33,49,54,108,119,123,126,131,145,149,154,157,292,297,317,323,327,330,455,459,482,488,492,495,538,542,562,567,571,575,578,625,629,632,750,754,768,773,777,781,784,860,864,867,1046,1050,1064,1069,1073,1173,1177,1181,1192,1196,1207,1211,1222,1226,1237,1241,1252,1256,1259,1330,1334,1337,1340,1343,1363,1368,1376,1408,1411,1417],[19,20,22],"h2",{"id":21},"the-problem-page-load-times-killing-user-experience","The Problem: Page Load Times Killing User Experience",[24,25,26],"p",{},"In 2021, Van Dale Rijmwoordenboek faced a critical user experience issue. Even after optimizing our phonetic search queries, page load times were still over 100ms. For a web application, this was unacceptable.",[24,28,29],{},[30,31,32],"strong",{},"The Challenge:",[34,35,36,40,43,46],"ul",{},[37,38,39],"li",{},"Page load times averaging 100-150ms",[37,41,42],{},"Users expecting instant page responses",[37,44,45],{},"Database queries running on every page load",[37,47,48],{},"No caching strategy in place",[24,50,51],{},[30,52,53],{},"The Numbers:",[55,56,61],"pre",{"className":57,"code":58,"language":59,"meta":60,"style":60},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- Every page load was hitting the database\nSELECT word, phonetic_code, frequency, definition\nFROM words \nWHERE phonetic_code LIKE 'KAT%'\nORDER BY frequency DESC\nLIMIT 20;\n-- Execution time: 85ms per page load\n","sql","",[62,63,64,72,78,84,90,96,102],"code",{"__ignoreMap":60},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,71],{},"-- Every page load was hitting the database\n",[65,73,75],{"class":67,"line":74},2,[65,76,77],{},"SELECT word, phonetic_code, frequency, definition\n",[65,79,81],{"class":67,"line":80},3,[65,82,83],{},"FROM words \n",[65,85,87],{"class":67,"line":86},4,[65,88,89],{},"WHERE phonetic_code LIKE 'KAT%'\n",[65,91,93],{"class":67,"line":92},5,[65,94,95],{},"ORDER BY frequency DESC\n",[65,97,99],{"class":67,"line":98},6,[65,100,101],{},"LIMIT 20;\n",[65,103,105],{"class":67,"line":104},7,[65,106,107],{},"-- Execution time: 85ms per page load\n",[24,109,110],{},[111,112],"img",{"alt":113,"className":114,"height":116,"src":117,"width":118},"Rijmwoordenboek caching performance",[115],"rounded-lg",600,"https:\u002F\u002Fpicsum.photos\u002Fid\u002F9\u002F1000\u002F600",1000,[19,120,122],{"id":121},"the-root-cause-no-caching-strategy","The Root Cause: No Caching Strategy",[24,124,125],{},"The problem was clear from our monitoring:",[24,127,128],{},[30,129,130],{},"What was happening:",[34,132,133,136,139,142],{},[37,134,135],{},"Every page load executed fresh database queries",[37,137,138],{},"No application-level caching implemented",[37,140,141],{},"Database was hit for identical queries repeatedly",[37,143,144],{},"Response times varied based on database load",[19,146,148],{"id":147},"the-solution-multi-layer-caching-strategy","The Solution: Multi-Layer Caching Strategy",[150,151,153],"h3",{"id":152},"step-1-application-level-caching-with-redis","Step 1: Application-Level Caching with Redis",[24,155,156],{},"The first breakthrough came with Redis caching:",[55,158,162],{"className":159,"code":160,"language":161,"meta":60,"style":60},"language-csharp shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F Application-level caching implementation\npublic async Task\u003CList\u003CWord>> GetWordsByPhoneticCode(string phoneticCode)\n{\n    var cacheKey = $\"words:phonetic:{phoneticCode}\";\n    \n    \u002F\u002F Try to get from cache first\n    var cachedWords = await _redis.GetAsync\u003CList\u003CWord>>(cacheKey);\n    if (cachedWords != null)\n    {\n        return cachedWords; \u002F\u002F Return cached result immediately\n    }\n    \n    \u002F\u002F If not in cache, query database\n    var words = await _database.QueryAsync\u003CWord>(\n        \"SELECT word, phonetic_code, frequency FROM words WHERE phonetic_code LIKE @code ORDER BY frequency DESC LIMIT 20\",\n        new { code = phoneticCode + \"%\" }\n    );\n    \n    \u002F\u002F Cache the result for 5 minutes\n    await _redis.SetAsync(cacheKey, words, TimeSpan.FromMinutes(5));\n    \n    return words;\n}\n","csharp",[62,163,164,169,174,179,184,189,194,199,205,211,217,223,228,234,240,246,252,258,263,269,275,280,286],{"__ignoreMap":60},[65,165,166],{"class":67,"line":68},[65,167,168],{},"\u002F\u002F Application-level caching implementation\n",[65,170,171],{"class":67,"line":74},[65,172,173],{},"public async Task\u003CList\u003CWord>> GetWordsByPhoneticCode(string phoneticCode)\n",[65,175,176],{"class":67,"line":80},[65,177,178],{},"{\n",[65,180,181],{"class":67,"line":86},[65,182,183],{},"    var cacheKey = $\"words:phonetic:{phoneticCode}\";\n",[65,185,186],{"class":67,"line":92},[65,187,188],{},"    \n",[65,190,191],{"class":67,"line":98},[65,192,193],{},"    \u002F\u002F Try to get from cache first\n",[65,195,196],{"class":67,"line":104},[65,197,198],{},"    var cachedWords = await _redis.GetAsync\u003CList\u003CWord>>(cacheKey);\n",[65,200,202],{"class":67,"line":201},8,[65,203,204],{},"    if (cachedWords != null)\n",[65,206,208],{"class":67,"line":207},9,[65,209,210],{},"    {\n",[65,212,214],{"class":67,"line":213},10,[65,215,216],{},"        return cachedWords; \u002F\u002F Return cached result immediately\n",[65,218,220],{"class":67,"line":219},11,[65,221,222],{},"    }\n",[65,224,226],{"class":67,"line":225},12,[65,227,188],{},[65,229,231],{"class":67,"line":230},13,[65,232,233],{},"    \u002F\u002F If not in cache, query database\n",[65,235,237],{"class":67,"line":236},14,[65,238,239],{},"    var words = await _database.QueryAsync\u003CWord>(\n",[65,241,243],{"class":67,"line":242},15,[65,244,245],{},"        \"SELECT word, phonetic_code, frequency FROM words WHERE phonetic_code LIKE @code ORDER BY frequency DESC LIMIT 20\",\n",[65,247,249],{"class":67,"line":248},16,[65,250,251],{},"        new { code = phoneticCode + \"%\" }\n",[65,253,255],{"class":67,"line":254},17,[65,256,257],{},"    );\n",[65,259,261],{"class":67,"line":260},18,[65,262,188],{},[65,264,266],{"class":67,"line":265},19,[65,267,268],{},"    \u002F\u002F Cache the result for 5 minutes\n",[65,270,272],{"class":67,"line":271},20,[65,273,274],{},"    await _redis.SetAsync(cacheKey, words, TimeSpan.FromMinutes(5));\n",[65,276,278],{"class":67,"line":277},21,[65,279,188],{},[65,281,283],{"class":67,"line":282},22,[65,284,285],{},"    return words;\n",[65,287,289],{"class":67,"line":288},23,[65,290,291],{},"}\n",[24,293,294],{},[30,295,296],{},"Why This Works:",[34,298,299,305,311,314],{},[37,300,301,304],{},[62,302,303],{},"Redis.GetAsync()",": Checks cache first, returns immediately if found",[37,306,307,310],{},[62,308,309],{},"TimeSpan.FromMinutes(5)",": Caches results for 5 minutes, balancing freshness with performance",[37,312,313],{},"Eliminates database hits for repeated queries",[37,315,316],{},"Dramatically reduces response times for cached data",[24,318,319,322],{},[30,320,321],{},"Immediate Result:"," Cached queries dropped from 85ms to 2ms (42x improvement)",[150,324,326],{"id":325},"step-2-database-query-result-caching","Step 2: Database Query Result Caching",[24,328,329],{},"For frequently accessed phonetic patterns, we implemented database-level caching:",[55,331,333],{"className":57,"code":332,"language":59,"meta":60,"style":60},"-- Create materialized view for common phonetic patterns\nCREATE MATERIALIZED VIEW words_common_patterns AS\nSELECT phonetic_code, \n       array_agg(word ORDER BY frequency DESC) as words,\n       array_agg(frequency ORDER BY frequency DESC) as frequencies\nFROM words \nWHERE phonetic_code IN (\n    SELECT phonetic_code \n    FROM words \n    GROUP BY phonetic_code \n    HAVING count(*) > 10\n)\nGROUP BY phonetic_code;\n\n-- Refresh materialized view every hour\nCREATE OR REPLACE FUNCTION refresh_words_patterns()\nRETURNS void AS $$\nBEGIN\n    REFRESH MATERIALIZED VIEW CONCURRENTLY words_common_patterns;\nEND;\n$$ LANGUAGE plpgsql;\n\n-- Schedule refresh every hour\nSELECT cron.schedule('refresh-words-patterns', '0 * * * *', 'SELECT refresh_words_patterns();');\n",[62,334,335,340,345,350,355,360,364,369,374,379,384,389,394,399,405,410,415,420,425,430,435,440,444,449],{"__ignoreMap":60},[65,336,337],{"class":67,"line":68},[65,338,339],{},"-- Create materialized view for common phonetic patterns\n",[65,341,342],{"class":67,"line":74},[65,343,344],{},"CREATE MATERIALIZED VIEW words_common_patterns AS\n",[65,346,347],{"class":67,"line":80},[65,348,349],{},"SELECT phonetic_code, \n",[65,351,352],{"class":67,"line":86},[65,353,354],{},"       array_agg(word ORDER BY frequency DESC) as words,\n",[65,356,357],{"class":67,"line":92},[65,358,359],{},"       array_agg(frequency ORDER BY frequency DESC) as frequencies\n",[65,361,362],{"class":67,"line":98},[65,363,83],{},[65,365,366],{"class":67,"line":104},[65,367,368],{},"WHERE phonetic_code IN (\n",[65,370,371],{"class":67,"line":201},[65,372,373],{},"    SELECT phonetic_code \n",[65,375,376],{"class":67,"line":207},[65,377,378],{},"    FROM words \n",[65,380,381],{"class":67,"line":213},[65,382,383],{},"    GROUP BY phonetic_code \n",[65,385,386],{"class":67,"line":219},[65,387,388],{},"    HAVING count(*) > 10\n",[65,390,391],{"class":67,"line":225},[65,392,393],{},")\n",[65,395,396],{"class":67,"line":230},[65,397,398],{},"GROUP BY phonetic_code;\n",[65,400,401],{"class":67,"line":236},[65,402,404],{"emptyLinePlaceholder":403},true,"\n",[65,406,407],{"class":67,"line":242},[65,408,409],{},"-- Refresh materialized view every hour\n",[65,411,412],{"class":67,"line":248},[65,413,414],{},"CREATE OR REPLACE FUNCTION refresh_words_patterns()\n",[65,416,417],{"class":67,"line":254},[65,418,419],{},"RETURNS void AS $$\n",[65,421,422],{"class":67,"line":260},[65,423,424],{},"BEGIN\n",[65,426,427],{"class":67,"line":265},[65,428,429],{},"    REFRESH MATERIALIZED VIEW CONCURRENTLY words_common_patterns;\n",[65,431,432],{"class":67,"line":271},[65,433,434],{},"END;\n",[65,436,437],{"class":67,"line":277},[65,438,439],{},"$$ LANGUAGE plpgsql;\n",[65,441,442],{"class":67,"line":282},[65,443,404],{"emptyLinePlaceholder":403},[65,445,446],{"class":67,"line":288},[65,447,448],{},"-- Schedule refresh every hour\n",[65,450,452],{"class":67,"line":451},24,[65,453,454],{},"SELECT cron.schedule('refresh-words-patterns', '0 * * * *', 'SELECT refresh_words_patterns();');\n",[24,456,457],{},[30,458,296],{},[34,460,461,467,473,479],{},[37,462,463,466],{},[62,464,465],{},"MATERIALIZED VIEW",": Pre-computes common phonetic patterns",[37,468,469,472],{},[62,470,471],{},"REFRESH MATERIALIZED VIEW CONCURRENTLY",": Updates without blocking reads",[37,474,475,478],{},[62,476,477],{},"cron.schedule()",": Automatically refreshes data every hour",[37,480,481],{},"Eliminates expensive GROUP BY operations for common queries",[24,483,484,487],{},[30,485,486],{},"Result:"," Common pattern queries improved to 15ms (5.7x improvement)",[150,489,491],{"id":490},"step-3-http-response-caching","Step 3: HTTP Response Caching",[24,493,494],{},"For static phonetic data, we implemented HTTP caching:",[55,496,498],{"className":159,"code":497,"language":161,"meta":60,"style":60},"\u002F\u002F HTTP response caching implementation\n[HttpGet(\"phonetic\u002F{phoneticCode}\")]\n[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any)]\npublic async Task\u003CIActionResult> GetWordsByPhonetic(string phoneticCode)\n{\n    var words = await GetWordsByPhoneticCode(phoneticCode);\n    return Ok(words);\n}\n",[62,499,500,505,510,515,520,524,529,534],{"__ignoreMap":60},[65,501,502],{"class":67,"line":68},[65,503,504],{},"\u002F\u002F HTTP response caching implementation\n",[65,506,507],{"class":67,"line":74},[65,508,509],{},"[HttpGet(\"phonetic\u002F{phoneticCode}\")]\n",[65,511,512],{"class":67,"line":80},[65,513,514],{},"[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any)]\n",[65,516,517],{"class":67,"line":86},[65,518,519],{},"public async Task\u003CIActionResult> GetWordsByPhonetic(string phoneticCode)\n",[65,521,522],{"class":67,"line":92},[65,523,178],{},[65,525,526],{"class":67,"line":98},[65,527,528],{},"    var words = await GetWordsByPhoneticCode(phoneticCode);\n",[65,530,531],{"class":67,"line":104},[65,532,533],{},"    return Ok(words);\n",[65,535,536],{"class":67,"line":201},[65,537,291],{},[24,539,540],{},[30,541,296],{},[34,543,544,550,556,559],{},[37,545,546,549],{},[62,547,548],{},"ResponseCache(Duration = 300)",": Caches HTTP responses for 5 minutes",[37,551,552,555],{},[62,553,554],{},"ResponseCacheLocation.Any",": Allows caching at any level (browser, CDN, proxy)",[37,557,558],{},"Reduces server load for repeated requests",[37,560,561],{},"Improves response times for users with cached responses",[24,563,564,566],{},[30,565,486],{}," HTTP response times improved to 8ms (12.5x improvement)",[19,568,570],{"id":569},"the-game-changer-smart-cache-invalidation","The Game Changer: Smart Cache Invalidation",[150,572,574],{"id":573},"the-problem-stale-data-issues","The Problem: Stale Data Issues",[24,576,577],{},"With caching in place, we faced stale data problems:",[55,579,581],{"className":159,"code":580,"language":161,"meta":60,"style":60},"\u002F\u002F Problem: Cache not invalidated when data changes\npublic async Task UpdateWordFrequency(int wordId, int newFrequency)\n{\n    await _database.ExecuteAsync(\n        \"UPDATE words SET frequency = @frequency WHERE id = @id\",\n        new { frequency = newFrequency, id = wordId }\n    );\n    \u002F\u002F Cache not invalidated - users see stale data!\n}\n",[62,582,583,588,593,597,602,607,612,616,621],{"__ignoreMap":60},[65,584,585],{"class":67,"line":68},[65,586,587],{},"\u002F\u002F Problem: Cache not invalidated when data changes\n",[65,589,590],{"class":67,"line":74},[65,591,592],{},"public async Task UpdateWordFrequency(int wordId, int newFrequency)\n",[65,594,595],{"class":67,"line":80},[65,596,178],{},[65,598,599],{"class":67,"line":86},[65,600,601],{},"    await _database.ExecuteAsync(\n",[65,603,604],{"class":67,"line":92},[65,605,606],{},"        \"UPDATE words SET frequency = @frequency WHERE id = @id\",\n",[65,608,609],{"class":67,"line":98},[65,610,611],{},"        new { frequency = newFrequency, id = wordId }\n",[65,613,614],{"class":67,"line":104},[65,615,257],{},[65,617,618],{"class":67,"line":201},[65,619,620],{},"    \u002F\u002F Cache not invalidated - users see stale data!\n",[65,622,623],{"class":67,"line":207},[65,624,291],{},[150,626,628],{"id":627},"the-solution-intelligent-cache-invalidation","The Solution: Intelligent Cache Invalidation",[24,630,631],{},"We implemented smart cache invalidation:",[55,633,635],{"className":159,"code":634,"language":161,"meta":60,"style":60},"\u002F\u002F Smart cache invalidation implementation\npublic async Task UpdateWordFrequency(int wordId, int newFrequency)\n{\n    \u002F\u002F Get the word to find its phonetic code\n    var word = await _database.QueryFirstAsync\u003CWord>(\n        \"SELECT phonetic_code FROM words WHERE id = @id\", \n        new { id = wordId }\n    );\n    \n    \u002F\u002F Update the database\n    await _database.ExecuteAsync(\n        \"UPDATE words SET frequency = @frequency WHERE id = @id\",\n        new { frequency = newFrequency, id = wordId }\n    );\n    \n    \u002F\u002F Invalidate related caches\n    var cacheKey = $\"words:phonetic:{word.PhoneticCode}\";\n    await _redis.RemoveAsync(cacheKey);\n    \n    \u002F\u002F Also invalidate pattern cache if this affects common patterns\n    if (newFrequency > 1000)\n    {\n        await _redis.RemoveAsync(\"words:common_patterns\");\n    }\n}\n",[62,636,637,642,646,650,655,660,665,670,674,678,683,687,691,695,699,703,708,713,718,722,727,732,736,741,745],{"__ignoreMap":60},[65,638,639],{"class":67,"line":68},[65,640,641],{},"\u002F\u002F Smart cache invalidation implementation\n",[65,643,644],{"class":67,"line":74},[65,645,592],{},[65,647,648],{"class":67,"line":80},[65,649,178],{},[65,651,652],{"class":67,"line":86},[65,653,654],{},"    \u002F\u002F Get the word to find its phonetic code\n",[65,656,657],{"class":67,"line":92},[65,658,659],{},"    var word = await _database.QueryFirstAsync\u003CWord>(\n",[65,661,662],{"class":67,"line":98},[65,663,664],{},"        \"SELECT phonetic_code FROM words WHERE id = @id\", \n",[65,666,667],{"class":67,"line":104},[65,668,669],{},"        new { id = wordId }\n",[65,671,672],{"class":67,"line":201},[65,673,257],{},[65,675,676],{"class":67,"line":207},[65,677,188],{},[65,679,680],{"class":67,"line":213},[65,681,682],{},"    \u002F\u002F Update the database\n",[65,684,685],{"class":67,"line":219},[65,686,601],{},[65,688,689],{"class":67,"line":225},[65,690,606],{},[65,692,693],{"class":67,"line":230},[65,694,611],{},[65,696,697],{"class":67,"line":236},[65,698,257],{},[65,700,701],{"class":67,"line":242},[65,702,188],{},[65,704,705],{"class":67,"line":248},[65,706,707],{},"    \u002F\u002F Invalidate related caches\n",[65,709,710],{"class":67,"line":254},[65,711,712],{},"    var cacheKey = $\"words:phonetic:{word.PhoneticCode}\";\n",[65,714,715],{"class":67,"line":260},[65,716,717],{},"    await _redis.RemoveAsync(cacheKey);\n",[65,719,720],{"class":67,"line":265},[65,721,188],{},[65,723,724],{"class":67,"line":271},[65,725,726],{},"    \u002F\u002F Also invalidate pattern cache if this affects common patterns\n",[65,728,729],{"class":67,"line":277},[65,730,731],{},"    if (newFrequency > 1000)\n",[65,733,734],{"class":67,"line":282},[65,735,210],{},[65,737,738],{"class":67,"line":288},[65,739,740],{},"        await _redis.RemoveAsync(\"words:common_patterns\");\n",[65,742,743],{"class":67,"line":451},[65,744,222],{},[65,746,748],{"class":67,"line":747},25,[65,749,291],{},[24,751,752],{},[30,753,296],{},[34,755,756,759,762,765],{},[37,757,758],{},"Invalidates specific phonetic code caches when data changes",[37,760,761],{},"Invalidates pattern caches when frequency thresholds change",[37,763,764],{},"Ensures users always see fresh data",[37,766,767],{},"Maintains cache performance benefits",[24,769,770,772],{},[30,771,486],{}," Cache hit rate improved to 95% while maintaining data freshness",[19,774,776],{"id":775},"the-final-optimization-preemptive-caching","The Final Optimization: Preemptive Caching",[150,778,780],{"id":779},"the-problem-cache-misses-during-peak-usage","The Problem: Cache Misses During Peak Usage",[24,782,783],{},"During peak usage, cache misses were still causing slow responses:",[55,785,787],{"className":159,"code":786,"language":161,"meta":60,"style":60},"\u002F\u002F Problem: Cache misses during peak usage\npublic async Task\u003CList\u003CWord>> GetWordsByPhoneticCode(string phoneticCode)\n{\n    var cacheKey = $\"words:phonetic:{phoneticCode}\";\n    var cachedWords = await _redis.GetAsync\u003CList\u003CWord>>(cacheKey);\n    \n    if (cachedWords == null)\n    {\n        \u002F\u002F Cache miss - slow database query during peak usage\n        var words = await _database.QueryAsync\u003CWord>(...);\n        await _redis.SetAsync(cacheKey, words, TimeSpan.FromMinutes(5));\n        return words;\n    }\n    \n    return cachedWords;\n}\n",[62,788,789,794,798,802,806,810,814,819,823,828,833,838,843,847,851,856],{"__ignoreMap":60},[65,790,791],{"class":67,"line":68},[65,792,793],{},"\u002F\u002F Problem: Cache misses during peak usage\n",[65,795,796],{"class":67,"line":74},[65,797,173],{},[65,799,800],{"class":67,"line":80},[65,801,178],{},[65,803,804],{"class":67,"line":86},[65,805,183],{},[65,807,808],{"class":67,"line":92},[65,809,198],{},[65,811,812],{"class":67,"line":98},[65,813,188],{},[65,815,816],{"class":67,"line":104},[65,817,818],{},"    if (cachedWords == null)\n",[65,820,821],{"class":67,"line":201},[65,822,210],{},[65,824,825],{"class":67,"line":207},[65,826,827],{},"        \u002F\u002F Cache miss - slow database query during peak usage\n",[65,829,830],{"class":67,"line":213},[65,831,832],{},"        var words = await _database.QueryAsync\u003CWord>(...);\n",[65,834,835],{"class":67,"line":219},[65,836,837],{},"        await _redis.SetAsync(cacheKey, words, TimeSpan.FromMinutes(5));\n",[65,839,840],{"class":67,"line":225},[65,841,842],{},"        return words;\n",[65,844,845],{"class":67,"line":230},[65,846,222],{},[65,848,849],{"class":67,"line":236},[65,850,188],{},[65,852,853],{"class":67,"line":242},[65,854,855],{},"    return cachedWords;\n",[65,857,858],{"class":67,"line":248},[65,859,291],{},[150,861,863],{"id":862},"the-solution-background-cache-warming","The Solution: Background Cache Warming",[24,865,866],{},"We implemented background cache warming:",[55,868,870],{"className":159,"code":869,"language":161,"meta":60,"style":60},"\u002F\u002F Background cache warming implementation\npublic class CacheWarmingService : BackgroundService\n{\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        while (!stoppingToken.IsCancellationRequested)\n        {\n            \u002F\u002F Get most popular phonetic codes\n            var popularCodes = await _database.QueryAsync\u003Cstring>(\n                \"SELECT phonetic_code FROM words GROUP BY phonetic_code ORDER BY count(*) DESC LIMIT 100\"\n            );\n            \n            \u002F\u002F Pre-warm cache for popular codes\n            foreach (var code in popularCodes)\n            {\n                var cacheKey = $\"words:phonetic:{code}\";\n                var exists = await _redis.ExistsAsync(cacheKey);\n                \n                if (!exists)\n                {\n                    var words = await _database.QueryAsync\u003CWord>(\n                        \"SELECT word, phonetic_code, frequency FROM words WHERE phonetic_code LIKE @code ORDER BY frequency DESC LIMIT 20\",\n                        new { code = code + \"%\" }\n                    );\n                    \n                    await _redis.SetAsync(cacheKey, words, TimeSpan.FromMinutes(5));\n                }\n            }\n            \n            \u002F\u002F Wait 10 minutes before next warming cycle\n            await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);\n        }\n    }\n}\n",[62,871,872,877,882,886,891,895,900,905,910,915,920,925,930,935,940,945,950,955,960,965,970,975,980,985,990,995,1001,1007,1013,1018,1024,1030,1036,1041],{"__ignoreMap":60},[65,873,874],{"class":67,"line":68},[65,875,876],{},"\u002F\u002F Background cache warming implementation\n",[65,878,879],{"class":67,"line":74},[65,880,881],{},"public class CacheWarmingService : BackgroundService\n",[65,883,884],{"class":67,"line":80},[65,885,178],{},[65,887,888],{"class":67,"line":86},[65,889,890],{},"    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n",[65,892,893],{"class":67,"line":92},[65,894,210],{},[65,896,897],{"class":67,"line":98},[65,898,899],{},"        while (!stoppingToken.IsCancellationRequested)\n",[65,901,902],{"class":67,"line":104},[65,903,904],{},"        {\n",[65,906,907],{"class":67,"line":201},[65,908,909],{},"            \u002F\u002F Get most popular phonetic codes\n",[65,911,912],{"class":67,"line":207},[65,913,914],{},"            var popularCodes = await _database.QueryAsync\u003Cstring>(\n",[65,916,917],{"class":67,"line":213},[65,918,919],{},"                \"SELECT phonetic_code FROM words GROUP BY phonetic_code ORDER BY count(*) DESC LIMIT 100\"\n",[65,921,922],{"class":67,"line":219},[65,923,924],{},"            );\n",[65,926,927],{"class":67,"line":225},[65,928,929],{},"            \n",[65,931,932],{"class":67,"line":230},[65,933,934],{},"            \u002F\u002F Pre-warm cache for popular codes\n",[65,936,937],{"class":67,"line":236},[65,938,939],{},"            foreach (var code in popularCodes)\n",[65,941,942],{"class":67,"line":242},[65,943,944],{},"            {\n",[65,946,947],{"class":67,"line":248},[65,948,949],{},"                var cacheKey = $\"words:phonetic:{code}\";\n",[65,951,952],{"class":67,"line":254},[65,953,954],{},"                var exists = await _redis.ExistsAsync(cacheKey);\n",[65,956,957],{"class":67,"line":260},[65,958,959],{},"                \n",[65,961,962],{"class":67,"line":265},[65,963,964],{},"                if (!exists)\n",[65,966,967],{"class":67,"line":271},[65,968,969],{},"                {\n",[65,971,972],{"class":67,"line":277},[65,973,974],{},"                    var words = await _database.QueryAsync\u003CWord>(\n",[65,976,977],{"class":67,"line":282},[65,978,979],{},"                        \"SELECT word, phonetic_code, frequency FROM words WHERE phonetic_code LIKE @code ORDER BY frequency DESC LIMIT 20\",\n",[65,981,982],{"class":67,"line":288},[65,983,984],{},"                        new { code = code + \"%\" }\n",[65,986,987],{"class":67,"line":451},[65,988,989],{},"                    );\n",[65,991,992],{"class":67,"line":747},[65,993,994],{},"                    \n",[65,996,998],{"class":67,"line":997},26,[65,999,1000],{},"                    await _redis.SetAsync(cacheKey, words, TimeSpan.FromMinutes(5));\n",[65,1002,1004],{"class":67,"line":1003},27,[65,1005,1006],{},"                }\n",[65,1008,1010],{"class":67,"line":1009},28,[65,1011,1012],{},"            }\n",[65,1014,1016],{"class":67,"line":1015},29,[65,1017,929],{},[65,1019,1021],{"class":67,"line":1020},30,[65,1022,1023],{},"            \u002F\u002F Wait 10 minutes before next warming cycle\n",[65,1025,1027],{"class":67,"line":1026},31,[65,1028,1029],{},"            await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);\n",[65,1031,1033],{"class":67,"line":1032},32,[65,1034,1035],{},"        }\n",[65,1037,1039],{"class":67,"line":1038},33,[65,1040,222],{},[65,1042,1044],{"class":67,"line":1043},34,[65,1045,291],{},[24,1047,1048],{},[30,1049,296],{},[34,1051,1052,1055,1058,1061],{},[37,1053,1054],{},"Pre-warms cache for most popular phonetic codes",[37,1056,1057],{},"Runs in background without affecting user requests",[37,1059,1060],{},"Reduces cache misses during peak usage",[37,1062,1063],{},"Ensures popular queries are always cached",[24,1065,1066,1068],{},[30,1067,486],{}," Cache hit rate improved to 98%, response times consistently under 15ms",[19,1070,1072],{"id":1071},"performance-results-summary","Performance Results Summary",[1074,1075,1076,1092],"table",{},[1077,1078,1079],"thead",{},[1080,1081,1082,1086,1089],"tr",{},[1083,1084,1085],"th",{},"Optimization Step",[1083,1087,1088],{},"Response Time",[1083,1090,1091],{},"Improvement",[1093,1094,1095,1109,1122,1135,1148,1159],"tbody",{},[1080,1096,1097,1103,1106],{},[1098,1099,1100],"td",{},[30,1101,1102],{},"Original (No Caching)",[1098,1104,1105],{},"100-150ms",[1098,1107,1108],{},"Baseline",[1080,1110,1111,1116,1119],{},[1098,1112,1113],{},[30,1114,1115],{},"Redis Application Caching",[1098,1117,1118],{},"2ms",[1098,1120,1121],{},"50-75x faster",[1080,1123,1124,1129,1132],{},[1098,1125,1126],{},[30,1127,1128],{},"Database Materialized Views",[1098,1130,1131],{},"15ms",[1098,1133,1134],{},"6.7-10x faster",[1080,1136,1137,1142,1145],{},[1098,1138,1139],{},[30,1140,1141],{},"HTTP Response Caching",[1098,1143,1144],{},"8ms",[1098,1146,1147],{},"12.5-18.8x faster",[1080,1149,1150,1155,1157],{},[1098,1151,1152],{},[30,1153,1154],{},"Smart Cache Invalidation",[1098,1156,1118],{},[1098,1158,1121],{},[1080,1160,1161,1166,1169],{},[1098,1162,1163],{},[30,1164,1165],{},"Background Cache Warming",[1098,1167,1168],{},"\u003C15ms",[1098,1170,1171],{},[30,1172,1134],{},[19,1174,1176],{"id":1175},"key-lessons-learned","Key Lessons Learned",[150,1178,1180],{"id":1179},"_1-multi-layer-caching-is-essential","1. Multi-Layer Caching Is Essential",[34,1182,1183,1186,1189],{},[37,1184,1185],{},"Application-level caching eliminates database hits",[37,1187,1188],{},"Database-level caching optimizes expensive queries",[37,1190,1191],{},"HTTP-level caching reduces server load",[150,1193,1195],{"id":1194},"_2-cache-invalidation-strategy-matters","2. Cache Invalidation Strategy Matters",[34,1197,1198,1201,1204],{},[37,1199,1200],{},"Smart invalidation ensures data freshness",[37,1202,1203],{},"Invalidate related caches when data changes",[37,1205,1206],{},"Balance cache performance with data accuracy",[150,1208,1210],{"id":1209},"_3-background-processing-prevents-cache-misses","3. Background Processing Prevents Cache Misses",[34,1212,1213,1216,1219],{},[37,1214,1215],{},"Pre-warm cache for popular queries",[37,1217,1218],{},"Run warming processes during off-peak hours",[37,1220,1221],{},"Monitor cache hit rates and adjust strategies",[150,1223,1225],{"id":1224},"_4-cache-duration-optimization","4. Cache Duration Optimization",[34,1227,1228,1231,1234],{},[37,1229,1230],{},"Balance freshness with performance",[37,1232,1233],{},"Longer cache times for stable data",[37,1235,1236],{},"Shorter cache times for frequently changing data",[150,1238,1240],{"id":1239},"_5-monitor-cache-performance","5. Monitor Cache Performance",[34,1242,1243,1246,1249],{},[37,1244,1245],{},"Track cache hit rates",[37,1247,1248],{},"Monitor response times",[37,1250,1251],{},"Adjust caching strategies based on usage patterns",[19,1253,1255],{"id":1254},"implementation-checklist","Implementation Checklist",[24,1257,1258],{},"If you're facing similar page load performance issues:",[34,1260,1263,1276,1285,1294,1303,1312,1321],{"className":1261},[1262],"contains-task-list",[37,1264,1267,1271,1272,1275],{"className":1265},[1266],"task-list-item",[1268,1269],"input",{"disabled":403,"type":1270},"checkbox"," ",[30,1273,1274],{},"Implement application-level caching",": Use Redis or similar",[37,1277,1279,1271,1281,1284],{"className":1278},[1266],[1268,1280],{"disabled":403,"type":1270},[30,1282,1283],{},"Add database-level caching",": Use materialized views for expensive queries",[37,1286,1288,1271,1290,1293],{"className":1287},[1266],[1268,1289],{"disabled":403,"type":1270},[30,1291,1292],{},"Implement HTTP response caching",": Cache static responses",[37,1295,1297,1271,1299,1302],{"className":1296},[1266],[1268,1298],{"disabled":403,"type":1270},[30,1300,1301],{},"Design cache invalidation strategy",": Ensure data freshness",[37,1304,1306,1271,1308,1311],{"className":1305},[1266],[1268,1307],{"disabled":403,"type":1270},[30,1309,1310],{},"Add background cache warming",": Pre-warm popular queries",[37,1313,1315,1271,1317,1320],{"className":1314},[1266],[1268,1316],{"disabled":403,"type":1270},[30,1318,1319],{},"Monitor cache performance",": Track hit rates and response times",[37,1322,1324,1271,1326,1329],{"className":1323},[1266],[1268,1325],{"disabled":403,"type":1270},[30,1327,1328],{},"Optimize cache durations",": Balance freshness with performance",[19,1331,1333],{"id":1332},"summary","Summary",[24,1335,1336],{},"Optimizing page load times requires a comprehensive caching strategy. By combining Redis application caching, database materialized views, HTTP response caching, smart cache invalidation, and background cache warming, we achieved consistent sub-15ms response times for Rijmwoordenboek.",[24,1338,1339],{},"The key was understanding that caching isn't just about storing data—it's about creating a multi-layer strategy that eliminates bottlenecks at every level while maintaining data freshness.",[24,1341,1342],{},"If this article helped you understand caching optimization, we can help you implement these techniques in your own applications. At Ludulicious, we specialize in:",[34,1344,1345,1351,1357],{},[37,1346,1347,1350],{},[30,1348,1349],{},"Caching Strategies",": Multi-layer caching solutions for optimal performance",[37,1352,1353,1356],{},[30,1354,1355],{},"Database Performance Optimization",": From slow queries to indexing strategies",[37,1358,1359,1362],{},[30,1360,1361],{},"Custom Development",": Tailored solutions for your specific use case",[24,1364,1365],{},[30,1366,1367],{},"Ready to optimize your page load times?",[24,1369,1370,1375],{},[1371,1372,1374],"a",{"href":1373},"\u002Fcontact","Contact us"," for a free consultation, or check out our other optimization guides:",[34,1377,1378,1384,1390,1396,1402],{},[37,1379,1380],{},[1371,1381,1383],{"href":1382},"\u002Fblog\u002Fpostgresql-performance-strategy","PostgreSQL Performance Tuning: Strategic Lessons from Production",[37,1385,1386],{},[1371,1387,1389],{"href":1388},"\u002Fblog\u002Fduikersgids-spatial-search-optimization","Duikersgids: How I Made Spatial Search 55x Faster",[37,1391,1392],{},[1371,1393,1395],{"href":1394},"\u002Fblog\u002Frijmwoordenboek-phonetic-search-optimization","Rijmwoordenboek: Solving the 3-Second Phonetic Search Problem",[37,1397,1398],{},[1371,1399,1401],{"href":1400},"\u002Fblog\u002Fupstreamads-fulltext-search-optimization","UpstreamAds: From 1.2s to 35ms Full-Text Search",[37,1403,1404],{},[1371,1405,1407],{"href":1406},"\u002Fblog\u002Fpostgresql-configuration-optimization","PostgreSQL Configuration: The Settings That Matter",[1409,1410],"hr",{},[24,1412,1413],{},[1414,1415,1416],"em",{},"This optimization case study is based on real production experience with Van Dale Rijmwoordenboek. All performance numbers are from actual production systems.",[1418,1419,1420],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":60,"searchDepth":74,"depth":74,"links":1422},[1423,1424,1425,1430,1434,1438,1439,1446,1447],{"id":21,"depth":74,"text":22},{"id":121,"depth":74,"text":122},{"id":147,"depth":74,"text":148,"children":1426},[1427,1428,1429],{"id":152,"depth":80,"text":153},{"id":325,"depth":80,"text":326},{"id":490,"depth":80,"text":491},{"id":569,"depth":74,"text":570,"children":1431},[1432,1433],{"id":573,"depth":80,"text":574},{"id":627,"depth":80,"text":628},{"id":775,"depth":74,"text":776,"children":1435},[1436,1437],{"id":779,"depth":80,"text":780},{"id":862,"depth":80,"text":863},{"id":1071,"depth":74,"text":1072},{"id":1175,"depth":74,"text":1176,"children":1440},[1441,1442,1443,1444,1445],{"id":1179,"depth":80,"text":1180},{"id":1194,"depth":80,"text":1195},{"id":1209,"depth":80,"text":1210},{"id":1224,"depth":80,"text":1225},{"id":1239,"depth":80,"text":1240},{"id":1254,"depth":74,"text":1255},{"id":1332,"depth":74,"text":1333},[1449,1349],"Database Optimization","2025-01-17","Learn how we optimized Rijmwoordenboek page load times from 100ms+ to under 15ms using application-level caching, database query optimization, and response time strategies.","md",{"src":1454},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F9\u002F640\u002F360",{"schema":1456},{"type":1457,"name":5,"description":1458,"image":1454,"author":1459,"datePublished":1450,"dateModified":1450,"publisher":1460,"steps":1463,"totalTime":1482,"estimatedCost":1483},"HowTo","Learn how to implement multi-layer caching strategies for web applications, achieving sub-15ms response times using Redis, application-level caching, and CDN optimization techniques.",{"name":8,"url":9},{"name":1461,"url":1462},"Ludulicious B.V.","https:\u002F\u002Fludulicious.nl",[1464,1467,1470,1473,1476,1479],{"name":1465,"text":1466},"Analyze Current Performance","Identify slow page load times and caching opportunities",{"name":1468,"text":1469},"Implement Redis Caching","Set up Redis for database query caching and session storage",{"name":1471,"text":1472},"Add Application-Level Caching","Implement in-memory caching for frequently accessed data",{"name":1474,"text":1475},"Configure CDN Caching","Set up CDN with appropriate cache headers for static content",{"name":1477,"text":1478},"Optimize Cache Invalidation","Implement smart cache invalidation strategies",{"name":1480,"text":1481},"Monitor Cache Performance","Track cache hit rates and response times","PT1D",{"currency":1484,"value":1485},"EUR","3000","\u002Fblog\u002Frijmwoordenboek-caching-optimization",{"title":5,"description":1451},"blog\u002F7.rijmwoordenboek-caching-optimization",[1490,1491,1088,1492,1493,1494,1495],"PostgreSQL","Application Caching","Performance Optimization","Redis","Query Optimization","Rijmwoordenboek","v8SJbxVjNgKwiD4YH1FP5dFIF_Tz5DqDuMP5AvlHhhc",[1498,1501],{"title":1395,"path":1394,"stem":1499,"description":1500,"children":-1},"blog\u002F6.rijmwoordenboek-phonetic-search-optimization","Learn how we optimized PostgreSQL phonetic search for Van Dale Rijmwoordenboek, reducing search times from 3.2 seconds to 85ms using multi-layered indexing strategies and B-tree deduplication.",{"title":1401,"path":1400,"stem":1502,"description":1503,"children":-1},"blog\u002F8.upstreamads-fulltext-search-optimization","Learn how we optimized PostgreSQL full-text search for UpstreamAds, reducing search times from 1.2 seconds to 35ms using pre-computed tsvector indexes, multi-language strategies, and partial indexing.",[]]