[{"data":1,"prerenderedAt":1699},["ShallowReactive",2],{"page-\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002Fcache-invalidation-strategies\u002F":3,"faq-schema-\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002Fcache-invalidation-strategies\u002F":1678},{"id":4,"title":5,"body":6,"description":1667,"extension":1668,"meta":1669,"navigation":542,"path":1674,"seo":1675,"stem":1676,"__hash__":1677},"content\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002Fcache-invalidation-strategies\u002Findex.md","Cache Invalidation Strategies for Python APIs: Choosing the Right Eviction Model",{"type":7,"value":8,"toc":1654},"minimark",[9,13,37,40,193,196,199,204,207,297,312,393,396,398,402,405,500,507,742,757,764,766,770,773,860,863,1135,1159,1175,1177,1181,1184,1276,1283,1418,1438,1440,1444,1489,1494,1497,1499,1503,1537,1545,1547,1551,1554,1556,1560,1566,1572,1588,1594,1600,1602,1606,1611,1631,1636,1650],[10,11,5],"h1",{"id":12},"cache-invalidation-strategies-for-python-apis-choosing-the-right-eviction-model",[14,15,16,17,21,22,25,26,31,32,36],"p",{},"Putting data into a cache is easy. Getting stale data ",[18,19,20],"em",{},"out"," at the right moment is the hard half, and it's where most caching bugs live. This page compares the four strategies you'll actually choose between — time-based TTL, write-through, event and tag-based invalidation, and explicit purge — and gives you concrete criteria for picking one ",[18,23,24],{},"per endpoint"," instead of applying a single rule everywhere. It assumes you already have a working cache; if not, start with ",[27,28,30],"a",{"href":29},"\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002F","caching Python API responses with Redis",", the parent guide this article extends, part of the broader work on ",[27,33,35],{"href":34},"\u002Fscaling-and-operating-production-python-apis\u002F","scaling and operating production Python APIs",".",[14,38,39],{},"The decision is really a single trade-off: how much consistency do you need, and how much complexity will you pay for it? Get that right and you avoid both stale-data complaints and a tangle of brittle eviction code nobody dares touch six months later.",[41,42,50,51,50,55,50,59,50,78,50,85,50,95,50,103,50,107,50,114,50,117,50,120,50,123,50,129,50,135,50,139,50,142,50,146,50,149,50,153,50,157,50,160,50,164,50,168,50,171,50,178,50,182,50,186,50,190],"svg",{"viewBox":43,"role":44,"ariaLabelledBy":45,"xmlns":48,"style":49},"0 0 720 320","img",[46,47],"cache-inval-t","cache-inval-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[52,53,54],"title",{"id":46},"Cache invalidation decision tree",[56,57,58],"desc",{"id":47},"A decision tree: tolerable staleness leads to TTL; writes you control lead to write-through; related keys lead to tag-based invalidation; otherwise explicit purge. Complexity rises from left to right.",[60,61,62,63,50],"defs",{},"\n    ",[64,65,72,73,62],"marker",{"id":66,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"cache-inval-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[74,75],"path",{"d":76,"fill":77},"M0 0 L10 5 L0 10 z","var(--c-text-muted)",[79,80],"rect",{"x":81,"y":81,"width":82,"height":83,"fill":84},"0","720","320","var(--c-surface)",[79,86],{"x":87,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"270","20","180","52","10","var(--c-surface-alt)","var(--c-blue)","stroke-width:2;",[96,97,102],"text",{"x":98,"y":99,"fill":100,"style":101},"360","42","var(--c-text)","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Data changed —",[96,104,106],{"x":98,"y":105,"fill":100,"style":101},"60","how do we evict?",[108,109],"line",{"x1":98,"y1":110,"x2":111,"y2":112,"stroke":77,"style":113},"72","110","120","stroke-width:2;marker-end:url(#cache-inval-arrow);",[108,115],{"x1":98,"y1":110,"x2":116,"y2":112,"stroke":77,"style":113},"300",[108,118],{"x1":98,"y1":110,"x2":119,"y2":112,"stroke":77,"style":113},"490",[108,121],{"x1":98,"y1":110,"x2":122,"y2":112,"stroke":77,"style":113},"650",[79,124],{"x":88,"y":125,"width":126,"height":127,"rx":91,"fill":92,"stroke":128,"style":94},"124","170","64","var(--c-teal)",[96,130,134],{"x":131,"y":132,"fill":77,"style":133},"105","150","text-anchor:middle;font-size:12;font-family:var(--font-sans);","staleness ok?",[96,136,138],{"x":131,"y":126,"fill":100,"style":137},"text-anchor:middle;font-size:14;font-family:var(--font-sans);","TTL",[79,140],{"x":141,"y":125,"width":126,"height":127,"rx":91,"fill":92,"stroke":128,"style":94},"210",[96,143,145],{"x":144,"y":132,"fill":77,"style":133},"295","you own the write?",[96,147,148],{"x":144,"y":126,"fill":100,"style":137},"Write-through",[79,150],{"x":151,"y":125,"width":126,"height":127,"rx":91,"fill":92,"stroke":152,"style":94},"400","var(--c-yellow)",[96,154,156],{"x":155,"y":132,"fill":77,"style":133},"485","related keys?",[96,158,159],{"x":155,"y":126,"fill":100,"style":137},"Tag-based",[79,161],{"x":162,"y":125,"width":111,"height":127,"rx":91,"fill":92,"stroke":163,"style":94},"590","var(--c-coral)",[96,165,167],{"x":166,"y":132,"fill":77,"style":133},"645","on demand?",[96,169,170],{"x":166,"y":126,"fill":100,"style":137},"Purge",[108,172],{"x1":173,"y1":174,"x2":175,"y2":174,"stroke":176,"style":177},"40","236","680","var(--c-border)","stroke-width:1.5;marker-end:url(#cache-inval-arrow);",[96,179,181],{"x":131,"y":180,"fill":77,"style":133},"264","weaker consistency",[96,183,185],{"x":131,"y":184,"fill":77,"style":133},"282","less code",[96,187,189],{"x":188,"y":180,"fill":77,"style":133},"600","stronger consistency",[96,191,192],{"x":188,"y":184,"fill":77,"style":133},"more operational cost",[14,194,195],{},"Read that tree left to right and it is really an ordering by cost: TTL asks nothing beyond an expiry number, purge asks the most. Everything worth arguing about lives in the middle two, and the honest answer for most endpoints is \"start on the left and only move right when a real problem forces you.\"",[197,198],"hr",{},[200,201,203],"h2",{"id":202},"the-four-strategies-side-by-side","The four strategies side by side",[14,205,206],{},"Each strategy answers \"when does a cached value disappear\" differently. The table sizes them on the axes that decide real builds — how fresh the data stays, how much code it adds, the worst-case staleness window, and the cost it imposes on every request or every write.",[208,209,210,229],"table",{},[211,212,213],"thead",{},[214,215,216,220,223,226],"tr",{},[217,218,219],"th",{},"Strategy",[217,221,222],{},"Consistency",[217,224,225],{},"Complexity",[217,227,228],{},"Staleness risk",[230,231,232,250,265,281],"tbody",{},[214,233,234,241,244,247],{},[235,236,237],"td",{},[238,239,240],"strong",{},"TTL (time-based)",[235,242,243],{},"Eventual",[235,245,246],{},"Very low",[235,248,249],{},"Up to the full TTL",[214,251,252,256,259,262],{},[235,253,254],{},[238,255,148],{},[235,257,258],{},"Strong on owned writes",[235,260,261],{},"Medium",[235,263,264],{},"Near zero for writes you control",[214,266,267,272,275,278],{},[235,268,269],{},[238,270,271],{},"Event\u002Ftag-based",[235,273,274],{},"Strong across related keys",[235,276,277],{},"High",[235,279,280],{},"Low; bounded by event delivery",[214,282,283,288,291,294],{},[235,284,285],{},[238,286,287],{},"Explicit purge",[235,289,290],{},"Strong but manual",[235,292,293],{},"Low to medium",[235,295,296],{},"Until someone purges",[14,298,299,300,303,304,307,308,311],{},"TTL is the floor everyone starts from — it needs zero invalidation logic because expiry ",[18,301,302],{},"is"," the eviction. The other three add active eviction to shrink the staleness window, at rising cost. The column that surprises people is \"staleness risk\" for TTL: it is not the ",[18,305,306],{},"average"," staleness, it is the ",[18,309,310],{},"worst case",". A write that lands one second after a fresh fetch is invisible for almost the entire TTL. The figure below makes that concrete — a 60-second TTL against a write that arrives ten seconds in.",[41,313,50,318,50,321,50,324,50,326,50,332,50,337,50,341,50,345,50,350,50,354,50,359,50,361,50,364,50,366,50,370,50,373,50,378,50,381,50,385,50,388],{"viewBox":314,"role":44,"ariaLabelledBy":315,"xmlns":48,"style":49},"0 0 720 300",[316,317],"cache-inval-tl-t","cache-inval-tl-d",[52,319,320],{"id":316},"Staleness window: TTL versus write-through",[56,322,323],{"id":317},"A timeline from 0 to 60 seconds. Under a 60-second TTL, a write at 10 seconds leaves 50 seconds of stale reads. Under write-through the stale window is roughly zero.",[79,325],{"x":81,"y":81,"width":82,"height":116,"fill":84},[96,327,331],{"x":328,"y":329,"fill":100,"style":330},"140","30","text-anchor:start;font-size:13;font-family:var(--font-sans);","Write lands at t = 10s, TTL = 60s",[108,333],{"x1":334,"y1":335,"x2":334,"y2":334,"stroke":163,"style":336},"230","48","stroke-width:1.5;stroke-dasharray:4 4;",[96,338,340],{"x":334,"y":339,"fill":163,"style":133},"62","write",[96,342,344],{"x":88,"y":343,"fill":100,"style":330},"104","TTL 60s",[79,346],{"x":328,"y":347,"width":348,"height":329,"rx":69,"fill":92,"stroke":176,"style":349},"86","540","stroke-width:1;",[79,351],{"x":334,"y":347,"width":352,"height":329,"rx":81,"fill":163,"opacity":353},"450","0.55",[96,355,358],{"x":356,"y":357,"fill":100,"style":133},"455","106","50s serving stale data",[96,360,148],{"x":88,"y":89,"fill":100,"style":330},[79,362],{"x":328,"y":363,"width":348,"height":329,"rx":69,"fill":92,"stroke":176,"style":349},"162",[79,365],{"x":334,"y":363,"width":91,"height":329,"rx":81,"fill":128},[96,367,369],{"x":98,"y":368,"fill":100,"style":133},"182","~0s stale — evicted on the write",[108,371],{"x1":328,"y1":334,"x2":175,"y2":334,"stroke":176,"style":372},"stroke-width:1.5;",[96,374,377],{"x":328,"y":375,"fill":77,"style":376},"252","text-anchor:middle;font-size:11;font-family:var(--font-sans);","0s",[96,379,380],{"x":334,"y":375,"fill":77,"style":376},"10s",[96,382,384],{"x":383,"y":375,"fill":77,"style":376},"410","30s",[96,386,387],{"x":175,"y":375,"fill":77,"style":376},"60s",[96,389,392],{"x":328,"y":390,"fill":77,"style":391},"278","text-anchor:start;font-size:11;font-family:var(--font-sans);","Average staleness under TTL is half the window; the worst case is the whole window.",[14,394,395],{},"The lesson is not \"TTL is bad.\" You tune the TTL to the staleness your product can absorb and reach for active eviction only when even the worst case is unacceptable. A pricing table that changes twice a day is fine at a 300-second TTL. An \"is this seat still available\" check is not.",[197,397],{},[200,399,401],{"id":400},"write-through-update-the-cache-when-you-update-the-source","Write-through: update the cache when you update the source",[14,403,404],{},"If your own API owns the write, you already know the exact moment data changes — so update or delete the cached key in the same handler. This keeps the cache consistent for mutations you control without waiting for a TTL to lapse. The sequence is short: mutate the source of truth first, then reconcile the cache, then respond.",[41,406,50,411,50,414,50,417,50,420,50,424,50,429,50,432,50,435,50,438,50,442,50,445,50,449,50,451,50,453,50,455,50,457,50,464,50,468,50,472,50,477,50,480,50,484,50,488,50,492,50,494,50,497],{"viewBox":407,"role":44,"ariaLabelledBy":408,"xmlns":48,"style":49},"0 0 720 340",[409,410],"cache-inval-wt-t","cache-inval-wt-d",[52,412,413],{"id":409},"Write-through request sequence",[56,415,416],{"id":410},"A client sends a PUT to the API. The API updates the database, gets a committed acknowledgement, sets the fresh value in Redis, then returns 200 OK to the client.",[79,418],{"x":81,"y":81,"width":82,"height":419,"fill":84},"340",[79,421],{"x":422,"y":88,"width":111,"height":173,"rx":423,"fill":92,"stroke":93,"style":94},"35","8",[96,425,428],{"x":426,"y":427,"fill":100,"style":101},"90","45","Client",[79,430],{"x":431,"y":88,"width":111,"height":173,"rx":423,"fill":92,"stroke":93,"style":94},"215",[96,433,434],{"x":87,"y":427,"fill":100,"style":101},"API",[79,436],{"x":437,"y":88,"width":111,"height":173,"rx":423,"fill":92,"stroke":128,"style":94},"415",[96,439,441],{"x":440,"y":427,"fill":100,"style":101},"470","Database",[79,443],{"x":444,"y":88,"width":426,"height":173,"rx":423,"fill":92,"stroke":163,"style":94},"595",[96,446,448],{"x":447,"y":427,"fill":100,"style":101},"640","Redis",[108,450],{"x1":426,"y1":105,"x2":426,"y2":83,"stroke":176,"style":336},[108,452],{"x1":87,"y1":105,"x2":87,"y2":83,"stroke":176,"style":336},[108,454],{"x1":440,"y1":105,"x2":440,"y2":83,"stroke":176,"style":336},[108,456],{"x1":447,"y1":105,"x2":447,"y2":83,"stroke":176,"style":336},[60,458,62,459,50],{},[64,460,72,462,62],{"id":461,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"cache-inval-wt-arrow",[74,463],{"d":76,"fill":77},[96,465,467],{"x":89,"y":466,"fill":100,"style":133},"95","PUT \u002Fproduct\u002F42",[108,469],{"x1":426,"y1":470,"x2":87,"y2":470,"stroke":77,"style":471},"102","stroke-width:2;marker-end:url(#cache-inval-wt-arrow);",[96,473,476],{"x":474,"y":475,"fill":100,"style":133},"370","145","UPDATE row",[108,478],{"x1":87,"y1":479,"x2":440,"y2":479,"stroke":77,"style":471},"152",[96,481,483],{"x":474,"y":482,"fill":77,"style":133},"195","committed",[108,485],{"x1":440,"y1":486,"x2":87,"y2":486,"stroke":77,"style":487},"202","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#cache-inval-wt-arrow);",[96,489,491],{"x":356,"y":490,"fill":100,"style":133},"245","SET key ex=300",[108,493],{"x1":87,"y1":375,"x2":447,"y2":375,"stroke":77,"style":471},[96,495,496],{"x":89,"y":144,"fill":77,"style":133},"200 OK",[108,498],{"x1":87,"y1":499,"x2":426,"y2":499,"stroke":77,"style":487},"302",[14,501,502,503,506],{},"Order matters here. Write to the database ",[18,504,505],{},"first"," and only touch the cache after the commit succeeds. Reverse it — cache first, then database — and a failed write leaves the cache holding a value that never reached durable storage, the worst kind of stale: confidently wrong. In code, that discipline looks like this:",[508,509,514],"pre",{"className":510,"code":511,"language":512,"meta":513,"style":513},"language-python shiki shiki-themes github-light github-dark","import json\nimport os\n\nimport redis.asyncio as redis\n\nCACHE_TTL = int(os.getenv(\"CACHE_TTL_SECONDS\", \"300\"))\nNAMESPACE = os.getenv(\"CACHE_NAMESPACE\", \"api:v1\")\n\n\nasync def update_product(client: redis.Redis, product_id: int, changes: dict) -> dict:\n    record = await write_product_to_db(product_id, changes)  # durable store first\n    key = f\"{NAMESPACE}:product:{product_id}\"\n    # Only refresh the cache once the write is committed.\n    await client.set(key, json.dumps(record), ex=CACHE_TTL)\n    return record\n","python","",[515,516,517,529,537,544,558,563,592,614,619,624,657,676,708,714,733],"code",{"__ignoreMap":513},[518,519,521,525],"span",{"class":108,"line":520},1,[518,522,524],{"class":523},"szBVR","import",[518,526,528],{"class":527},"sVt8B"," json\n",[518,530,532,534],{"class":108,"line":531},2,[518,533,524],{"class":523},[518,535,536],{"class":527}," os\n",[518,538,540],{"class":108,"line":539},3,[518,541,543],{"emptyLinePlaceholder":542},true,"\n",[518,545,547,549,552,555],{"class":108,"line":546},4,[518,548,524],{"class":523},[518,550,551],{"class":527}," redis.asyncio ",[518,553,554],{"class":523},"as",[518,556,557],{"class":527}," redis\n",[518,559,561],{"class":108,"line":560},5,[518,562,543],{"emptyLinePlaceholder":542},[518,564,566,570,573,576,579,583,586,589],{"class":108,"line":565},6,[518,567,569],{"class":568},"sj4cs","CACHE_TTL",[518,571,572],{"class":523}," =",[518,574,575],{"class":568}," int",[518,577,578],{"class":527},"(os.getenv(",[518,580,582],{"class":581},"sZZnC","\"CACHE_TTL_SECONDS\"",[518,584,585],{"class":527},", ",[518,587,588],{"class":581},"\"300\"",[518,590,591],{"class":527},"))\n",[518,593,595,598,600,603,606,608,611],{"class":108,"line":594},7,[518,596,597],{"class":568},"NAMESPACE",[518,599,572],{"class":523},[518,601,602],{"class":527}," os.getenv(",[518,604,605],{"class":581},"\"CACHE_NAMESPACE\"",[518,607,585],{"class":527},[518,609,610],{"class":581},"\"api:v1\"",[518,612,613],{"class":527},")\n",[518,615,617],{"class":108,"line":616},8,[518,618,543],{"emptyLinePlaceholder":542},[518,620,622],{"class":108,"line":621},9,[518,623,543],{"emptyLinePlaceholder":542},[518,625,627,630,633,637,640,643,646,649,652,654],{"class":108,"line":626},10,[518,628,629],{"class":523},"async",[518,631,632],{"class":523}," def",[518,634,636],{"class":635},"sScJk"," update_product",[518,638,639],{"class":527},"(client: redis.Redis, product_id: ",[518,641,642],{"class":568},"int",[518,644,645],{"class":527},", changes: ",[518,647,648],{"class":568},"dict",[518,650,651],{"class":527},") -> ",[518,653,648],{"class":568},[518,655,656],{"class":527},":\n",[518,658,660,663,666,669,672],{"class":108,"line":659},11,[518,661,662],{"class":527},"    record ",[518,664,665],{"class":523},"=",[518,667,668],{"class":523}," await",[518,670,671],{"class":527}," write_product_to_db(product_id, changes)  ",[518,673,675],{"class":674},"sJ8bj","# durable store first\n",[518,677,679,682,684,687,690,693,696,699,702,705],{"class":108,"line":678},12,[518,680,681],{"class":527},"    key ",[518,683,665],{"class":523},[518,685,686],{"class":523}," f",[518,688,689],{"class":581},"\"",[518,691,692],{"class":568},"{NAMESPACE}",[518,694,695],{"class":581},":product:",[518,697,698],{"class":568},"{",[518,700,701],{"class":527},"product_id",[518,703,704],{"class":568},"}",[518,706,707],{"class":581},"\"\n",[518,709,711],{"class":108,"line":710},13,[518,712,713],{"class":674},"    # Only refresh the cache once the write is committed.\n",[518,715,717,720,723,727,729,731],{"class":108,"line":716},14,[518,718,719],{"class":523},"    await",[518,721,722],{"class":527}," client.set(key, json.dumps(record), ",[518,724,726],{"class":725},"s4XuR","ex",[518,728,665],{"class":523},[518,730,569],{"class":568},[518,732,613],{"class":527},[518,734,736,739],{"class":108,"line":735},15,[518,737,738],{"class":523},"    return",[518,740,741],{"class":527}," record\n",[14,743,744,745,748,749,752,753,756],{},"Many teams prefer write-",[18,746,747],{},"invalidate"," — ",[515,750,751],{},"await client.delete(key)"," instead of re-setting — so the next read lazily repopulates with whatever the read path computes. That avoids subtle drift between your write payload and the shape the read endpoint returns, a real bug source when the read view joins in extra fields (a computed ",[515,754,755],{},"in_stock"," flag, a formatted price) the write handler doesn't have in scope. Delete-on-write fails safe and is my default; set-on-write only wins for a hot key where you can't afford the brief post-invalidation miss.",[14,758,759,760,763],{},"Write-through only helps for writes ",[18,761,762],{},"you"," observe. The moment a value changes behind your back — an admin editing rows directly, a nightly batch job, a partner API pushing updates — the write handler never fires and the cache silently goes stale. That is exactly the gap event-based invalidation closes.",[197,765],{},[200,767,769],{"id":768},"event-and-tag-based-invalidate-groups-of-related-keys","Event and tag-based: invalidate groups of related keys",[14,771,772],{},"One write often invalidates many cached responses. Updating a single product should evict the product detail, the category listing it appears in, and the search results that surface it. Tracking that fan-out by hand — remembering every derived key at every write site — is fragile and quietly rots as you add endpoints. Instead, record which keys belong to a tag in a Redis set when you cache them, then drop the whole group in one call when the underlying entity changes.",[41,774,50,778,50,781,50,784,50,786,50,793,50,797,50,801,50,805,50,809,50,814,50,817,50,820,50,824,50,828,50,830,50,833,50,837,50,842,50,845,50,849,50,852,50,856],{"viewBox":43,"role":44,"ariaLabelledBy":775,"xmlns":48,"style":49},[776,777],"cache-inval-tag-t","cache-inval-tag-d",[52,779,780],{"id":776},"Tag-based fan-out invalidation",[56,782,783],{"id":777},"A write to product 42 calls invalidate_tag on the category:shoes tag set, which drops three derived cached views at once: the product detail, the category listing, and a search result.",[79,785],{"x":81,"y":81,"width":82,"height":83,"fill":84},[60,787,62,788,50],{},[64,789,72,791,62],{"id":790,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"cache-inval-tag-arrow",[74,792],{"d":76,"fill":77},[79,794],{"x":88,"y":795,"width":132,"height":796,"rx":91,"fill":92,"stroke":163,"style":94},"132","56",[96,798,800],{"x":466,"y":799,"fill":77,"style":376},"156","UPDATE",[96,802,804],{"x":466,"y":803,"fill":100,"style":101},"174","product:42",[96,806,808],{"x":807,"y":132,"fill":77,"style":376},"222","invalidate_tag()",[108,810],{"x1":126,"y1":811,"x2":812,"y2":811,"stroke":77,"style":813},"160","268","stroke-width:2;marker-end:url(#cache-inval-tag-arrow);",[79,815],{"x":87,"y":816,"width":89,"height":127,"rx":91,"fill":92,"stroke":152,"style":94},"128",[96,818,819],{"x":98,"y":479,"fill":77,"style":376},"SET tag:category:shoes",[96,821,823],{"x":98,"y":822,"fill":100,"style":133},"172","SMEMBERS + DEL",[108,825],{"x1":352,"y1":811,"x2":826,"y2":827,"stroke":77,"style":813},"528","66",[108,829],{"x1":352,"y1":811,"x2":826,"y2":811,"stroke":77,"style":813},[108,831],{"x1":352,"y1":811,"x2":826,"y2":832,"stroke":77,"style":813},"254",[79,834],{"x":835,"y":836,"width":89,"height":836,"rx":423,"fill":92,"stroke":128,"style":94},"530","44",[96,838,841],{"x":839,"y":840,"fill":100,"style":133},"620","71","product:42 detail",[79,843],{"x":835,"y":844,"width":89,"height":836,"rx":423,"fill":92,"stroke":128,"style":94},"138",[96,846,848],{"x":839,"y":847,"fill":100,"style":133},"165","listing:shoes",[79,850],{"x":835,"y":851,"width":89,"height":836,"rx":423,"fill":92,"stroke":128,"style":94},"232",[96,853,855],{"x":839,"y":854,"fill":100,"style":133},"259","search:running-shoes",[96,857,859],{"x":98,"y":858,"fill":77,"style":376},"298","One SMEMBERS read and one DEL drop every derived view together.",[14,861,862],{},"The implementation is two small helpers — one that tags on write, one that invalidates a whole tag on change:",[508,864,866],{"className":510,"code":865,"language":512,"meta":513,"style":513},"import redis.asyncio as redis\n\n\nasync def cache_with_tag(\n    client: redis.Redis, key: str, value: str, tag: str, ttl: int\n) -> None:\n    async with client.pipeline(transaction=True) as pipe:\n        pipe.set(key, value, ex=ttl)\n        pipe.sadd(f\"tag:{tag}\", key)\n        pipe.expire(f\"tag:{tag}\", ttl)\n        await pipe.execute()\n\n\nasync def invalidate_tag(client: redis.Redis, tag: str) -> int:\n    members = await client.smembers(f\"tag:{tag}\")\n    if not members:\n        return 0\n    await client.delete(*members, f\"tag:{tag}\")\n    return len(members)\n",[515,867,868,878,882,886,898,922,931,958,970,993,1013,1021,1025,1029,1049,1075,1087,1096,1124],{"__ignoreMap":513},[518,869,870,872,874,876],{"class":108,"line":520},[518,871,524],{"class":523},[518,873,551],{"class":527},[518,875,554],{"class":523},[518,877,557],{"class":527},[518,879,880],{"class":108,"line":531},[518,881,543],{"emptyLinePlaceholder":542},[518,883,884],{"class":108,"line":539},[518,885,543],{"emptyLinePlaceholder":542},[518,887,888,890,892,895],{"class":108,"line":546},[518,889,629],{"class":523},[518,891,632],{"class":523},[518,893,894],{"class":635}," cache_with_tag",[518,896,897],{"class":527},"(\n",[518,899,900,903,906,909,911,914,916,919],{"class":108,"line":560},[518,901,902],{"class":527},"    client: redis.Redis, key: ",[518,904,905],{"class":568},"str",[518,907,908],{"class":527},", value: ",[518,910,905],{"class":568},[518,912,913],{"class":527},", tag: ",[518,915,905],{"class":568},[518,917,918],{"class":527},", ttl: ",[518,920,921],{"class":568},"int\n",[518,923,924,926,929],{"class":108,"line":565},[518,925,651],{"class":527},[518,927,928],{"class":568},"None",[518,930,656],{"class":527},[518,932,933,936,939,942,945,947,950,953,955],{"class":108,"line":594},[518,934,935],{"class":523},"    async",[518,937,938],{"class":523}," with",[518,940,941],{"class":527}," client.pipeline(",[518,943,944],{"class":725},"transaction",[518,946,665],{"class":523},[518,948,949],{"class":568},"True",[518,951,952],{"class":527},") ",[518,954,554],{"class":523},[518,956,957],{"class":527}," pipe:\n",[518,959,960,963,965,967],{"class":108,"line":616},[518,961,962],{"class":527},"        pipe.set(key, value, ",[518,964,726],{"class":725},[518,966,665],{"class":523},[518,968,969],{"class":527},"ttl)\n",[518,971,972,975,978,981,983,986,988,990],{"class":108,"line":621},[518,973,974],{"class":527},"        pipe.sadd(",[518,976,977],{"class":523},"f",[518,979,980],{"class":581},"\"tag:",[518,982,698],{"class":568},[518,984,985],{"class":527},"tag",[518,987,704],{"class":568},[518,989,689],{"class":581},[518,991,992],{"class":527},", key)\n",[518,994,995,998,1000,1002,1004,1006,1008,1010],{"class":108,"line":626},[518,996,997],{"class":527},"        pipe.expire(",[518,999,977],{"class":523},[518,1001,980],{"class":581},[518,1003,698],{"class":568},[518,1005,985],{"class":527},[518,1007,704],{"class":568},[518,1009,689],{"class":581},[518,1011,1012],{"class":527},", ttl)\n",[518,1014,1015,1018],{"class":108,"line":659},[518,1016,1017],{"class":523},"        await",[518,1019,1020],{"class":527}," pipe.execute()\n",[518,1022,1023],{"class":108,"line":678},[518,1024,543],{"emptyLinePlaceholder":542},[518,1026,1027],{"class":108,"line":710},[518,1028,543],{"emptyLinePlaceholder":542},[518,1030,1031,1033,1035,1038,1041,1043,1045,1047],{"class":108,"line":716},[518,1032,629],{"class":523},[518,1034,632],{"class":523},[518,1036,1037],{"class":635}," invalidate_tag",[518,1039,1040],{"class":527},"(client: redis.Redis, tag: ",[518,1042,905],{"class":568},[518,1044,651],{"class":527},[518,1046,642],{"class":568},[518,1048,656],{"class":527},[518,1050,1051,1054,1056,1058,1061,1063,1065,1067,1069,1071,1073],{"class":108,"line":735},[518,1052,1053],{"class":527},"    members ",[518,1055,665],{"class":523},[518,1057,668],{"class":523},[518,1059,1060],{"class":527}," client.smembers(",[518,1062,977],{"class":523},[518,1064,980],{"class":581},[518,1066,698],{"class":568},[518,1068,985],{"class":527},[518,1070,704],{"class":568},[518,1072,689],{"class":581},[518,1074,613],{"class":527},[518,1076,1078,1081,1084],{"class":108,"line":1077},16,[518,1079,1080],{"class":523},"    if",[518,1082,1083],{"class":523}," not",[518,1085,1086],{"class":527}," members:\n",[518,1088,1090,1093],{"class":108,"line":1089},17,[518,1091,1092],{"class":523},"        return",[518,1094,1095],{"class":568}," 0\n",[518,1097,1099,1101,1104,1107,1110,1112,1114,1116,1118,1120,1122],{"class":108,"line":1098},18,[518,1100,719],{"class":523},[518,1102,1103],{"class":527}," client.delete(",[518,1105,1106],{"class":523},"*",[518,1108,1109],{"class":527},"members, ",[518,1111,977],{"class":523},[518,1113,980],{"class":581},[518,1115,698],{"class":568},[518,1117,985],{"class":527},[518,1119,704],{"class":568},[518,1121,689],{"class":581},[518,1123,613],{"class":527},[518,1125,1127,1129,1132],{"class":108,"line":1126},19,[518,1128,738],{"class":523},[518,1130,1131],{"class":568}," len",[518,1133,1134],{"class":527},"(members)\n",[14,1136,1137,1138,1140,1141,1144,1145,1149,1150,1153,1154,1158],{},"When ",[515,1139,804],{}," changes, call ",[515,1142,1143],{},"invalidate_tag(client, \"category:shoes\")"," and every cached response tagged with that category vanishes in one round trip. The natural trigger is often an event you already receive — a database change stream, a queue message, or a webhook from upstream. If a partner pushes updates to you, wiring their ",[27,1146,1148],{"href":1147},"\u002Fautomating-side-hustle-operations-with-apis\u002Fprocessing-webhooks-with-python\u002F","webhook into a Python handler"," that calls ",[515,1151,1152],{},"invalidate_tag"," gives you event-driven freshness for data you don't own; pair it with an ",[27,1155,1157],{"href":1156},"\u002Fautomating-side-hustle-operations-with-apis\u002Fprocessing-webhooks-with-python\u002Fbuilding-an-idempotent-webhook-receiver\u002F","idempotent webhook receiver"," so a duplicated delivery doesn't matter.",[14,1160,1161,1162,1165,1166,1170,1171,1174],{},"Two production notes. First, when each instance keeps its own in-process layer, broadcast the invalidation over Redis pub\u002Fsub so every replica drops its local view; with a single shared Redis, the ",[515,1163,1164],{},"DEL"," above already invalidates for everyone. The ",[27,1167,1169],{"href":1168},"\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002Fredis-vs-in-memory-caching-for-fastapi\u002F","Redis versus in-memory caching"," comparison spells out when that second layer earns its keep. Second, never use ",[515,1172,1173],{},"KEYS pattern"," to find related keys — it scans the whole keyspace and blocks the single-threaded event loop while it runs. The tag set exists precisely so you never have to scan.",[197,1176],{},[200,1178,1180],{"id":1179},"explicit-purge-flush-on-demand","Explicit purge: flush on demand",[14,1182,1183],{},"Sometimes the trigger is human or external — a CMS publish, a manual \"clear cache\" button in your admin, a deploy hook. Expose a guarded endpoint or a CLI that purges a namespace, and bump a version suffix when you need a clean break. There are two mechanics worth knowing, shown side by side below: walk-and-delete for a targeted clear, and namespace rotation for an instant one.",[41,1185,50,1189,50,1192,50,1195,50,1197,50,1204,50,1208,50,1211,50,1214,50,1216,50,1221,50,1226,50,1228,50,1232,50,1235,50,1237,50,1239,50,1242,50,1246,50,1250,50,1253,50,1257,50,1262,50,1265,50,1269,50,1273],{"viewBox":314,"role":44,"ariaLabelledBy":1186,"xmlns":48,"style":49},[1187,1188],"cache-inval-purge-t","cache-inval-purge-d",[52,1190,1191],{"id":1187},"Namespace rotation versus scan-and-delete purge",[56,1193,1194],{"id":1188},"Left: scan_iter walks matching keys in cursor batches and deletes each. Right: rotating the namespace from api:v1 to api:v2 makes every new read miss instantly while old keys age out on their own TTL.",[79,1196],{"x":81,"y":81,"width":82,"height":116,"fill":84},[60,1198,62,1199,50],{},[64,1200,72,1202,62],{"id":1201,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"cache-inval-purge-arrow",[74,1203],{"d":76,"fill":77},[96,1205,1207],{"x":89,"y":1206,"fill":100,"style":101},"34","scan_iter + delete",[96,1209,1210],{"x":348,"y":1206,"fill":100,"style":101},"rotate namespace",[108,1212],{"x1":98,"y1":88,"x2":98,"y2":1213,"stroke":176,"style":336},"280",[79,1215],{"x":173,"y":105,"width":112,"height":173,"rx":423,"fill":92,"stroke":163,"style":94},[96,1217,1220],{"x":1218,"y":1219,"fill":100,"style":133},"100","85","cursor batch",[108,1222],{"x1":811,"y1":1223,"x2":1224,"y2":1223,"stroke":77,"style":1225},"80","205","stroke-width:2;marker-end:url(#cache-inval-purge-arrow);",[79,1227],{"x":141,"y":105,"width":111,"height":173,"rx":423,"fill":92,"stroke":163,"style":94},[96,1229,1231],{"x":1230,"y":1219,"fill":100,"style":133},"265","DEL key",[108,1233],{"x1":1230,"y1":1218,"x2":1230,"y2":1234,"stroke":77,"style":1225},"135",[108,1236],{"x1":1230,"y1":1234,"x2":1218,"y2":1234,"stroke":77,"style":94},[108,1238],{"x1":1218,"y1":1234,"x2":1218,"y2":1218,"stroke":77,"style":1225},[96,1240,1241],{"x":89,"y":126,"fill":77,"style":376},"loops until cursor is 0",[96,1243,1245],{"x":89,"y":1244,"fill":77,"style":376},"200","non-blocking, O(matched keys)",[79,1247],{"x":1248,"y":105,"width":1249,"height":173,"rx":423,"fill":92,"stroke":128,"style":94},"430","220",[96,1251,1252],{"x":348,"y":1219,"fill":100,"style":133},"CACHE_NAMESPACE: api:v1 → api:v2",[79,1254],{"x":1248,"y":112,"width":131,"height":1255,"rx":423,"fill":84,"stroke":176,"style":1256},"36","stroke-width:1.5;stroke-dasharray:4 3;",[96,1258,1261],{"x":1259,"y":1260,"fill":77,"style":133},"482","143","api:v1 (old)",[79,1263],{"x":1264,"y":112,"width":131,"height":1255,"rx":423,"fill":92,"stroke":128,"style":94},"545",[96,1266,1268],{"x":1267,"y":1260,"fill":100,"style":133},"597","api:v2 (fresh)",[96,1270,1272],{"x":348,"y":1271,"fill":77,"style":376},"188","reads hit v2 at once; v1 keys age out on TTL",[96,1274,1275],{"x":348,"y":141,"fill":77,"style":376},"zero scan, zero blocking",[14,1277,1278,1279,1282],{},"The scan-and-delete helper walks the keyspace in cursor-based batches instead of blocking like ",[515,1280,1281],{},"KEYS",":",[508,1284,1286],{"className":510,"code":1285,"language":512,"meta":513,"style":513},"import os\n\nimport redis.asyncio as redis\n\n\nasync def purge_namespace(client: redis.Redis, namespace: str) -> int:\n    deleted = 0\n    async for key in client.scan_iter(match=f\"{namespace}:*\", count=500):\n        await client.delete(key)\n        deleted += 1\n    return deleted\n",[515,1287,1288,1294,1298,1308,1312,1316,1336,1345,1393,1400,1411],{"__ignoreMap":513},[518,1289,1290,1292],{"class":108,"line":520},[518,1291,524],{"class":523},[518,1293,536],{"class":527},[518,1295,1296],{"class":108,"line":531},[518,1297,543],{"emptyLinePlaceholder":542},[518,1299,1300,1302,1304,1306],{"class":108,"line":539},[518,1301,524],{"class":523},[518,1303,551],{"class":527},[518,1305,554],{"class":523},[518,1307,557],{"class":527},[518,1309,1310],{"class":108,"line":546},[518,1311,543],{"emptyLinePlaceholder":542},[518,1313,1314],{"class":108,"line":560},[518,1315,543],{"emptyLinePlaceholder":542},[518,1317,1318,1320,1322,1325,1328,1330,1332,1334],{"class":108,"line":565},[518,1319,629],{"class":523},[518,1321,632],{"class":523},[518,1323,1324],{"class":635}," purge_namespace",[518,1326,1327],{"class":527},"(client: redis.Redis, namespace: ",[518,1329,905],{"class":568},[518,1331,651],{"class":527},[518,1333,642],{"class":568},[518,1335,656],{"class":527},[518,1337,1338,1341,1343],{"class":108,"line":594},[518,1339,1340],{"class":527},"    deleted ",[518,1342,665],{"class":523},[518,1344,1095],{"class":568},[518,1346,1347,1349,1352,1355,1358,1361,1364,1366,1368,1370,1372,1375,1377,1380,1382,1385,1387,1390],{"class":108,"line":616},[518,1348,935],{"class":523},[518,1350,1351],{"class":523}," for",[518,1353,1354],{"class":527}," key ",[518,1356,1357],{"class":523},"in",[518,1359,1360],{"class":527}," client.scan_iter(",[518,1362,1363],{"class":725},"match",[518,1365,665],{"class":523},[518,1367,977],{"class":523},[518,1369,689],{"class":581},[518,1371,698],{"class":568},[518,1373,1374],{"class":527},"namespace",[518,1376,704],{"class":568},[518,1378,1379],{"class":581},":*\"",[518,1381,585],{"class":527},[518,1383,1384],{"class":725},"count",[518,1386,665],{"class":523},[518,1388,1389],{"class":568},"500",[518,1391,1392],{"class":527},"):\n",[518,1394,1395,1397],{"class":108,"line":621},[518,1396,1017],{"class":523},[518,1398,1399],{"class":527}," client.delete(key)\n",[518,1401,1402,1405,1408],{"class":108,"line":626},[518,1403,1404],{"class":527},"        deleted ",[518,1406,1407],{"class":523},"+=",[518,1409,1410],{"class":568}," 1\n",[518,1412,1413,1415],{"class":108,"line":659},[518,1414,738],{"class":523},[518,1416,1417],{"class":527}," deleted\n",[14,1419,1420,1421,1424,1425,1428,1429,1432,1433,1437],{},"For an instant, zero-scan purge, simply rotate ",[515,1422,1423],{},"CACHE_NAMESPACE"," from ",[515,1426,1427],{},"api:v1"," to ",[515,1430,1431],{},"api:v2"," on deploy — old keys age out on their own TTL while every new read uses the fresh namespace. This is the trick after a schema or serialization-format change, where every cached blob is now shaped wrong and you want them gone the moment new code ships. If a scan sweep is large and you don't want it on the request path, hand it to a ",[27,1434,1436],{"href":1435},"\u002Fscaling-and-operating-production-python-apis\u002Frunning-background-jobs-with-celery\u002F","background job with Celery"," so a \"clear cache\" click returns immediately and the deletion runs off to the side.",[197,1439],{},[200,1441,1443],{"id":1442},"when-to-choose-when-to-avoid","When to choose, when to avoid",[1445,1446,1447,1462,1471,1480],"ul",{},[1448,1449,1450,1453,1454,1457,1458,1461],"li",{},[238,1451,1452],{},"Choose TTL"," when data is read-mostly and a few seconds or minutes of staleness is acceptable — pricing snapshots, dashboards, public listings. ",[238,1455,1456],{},"Avoid it"," as your ",[18,1459,1460],{},"only"," tool when a stale read causes a real error, like serving a deleted record or a revoked permission.",[1448,1463,1464,1467,1468,1470],{},[238,1465,1466],{},"Choose write-through"," when your service owns the writes and the data is read far more than written. ",[238,1469,1456],{}," when writes come from systems you don't control — you'll never see the event to act on, and the cache will lie.",[1448,1472,1473,1476,1477,1479],{},[238,1474,1475],{},"Choose tag-based"," when one write invalidates many derived responses and correctness across them matters. ",[238,1478,1456],{}," for simple key-per-resource caches where the tag bookkeeping costs more than it saves.",[1448,1481,1482,1485,1486,1488],{},[238,1483,1484],{},"Choose explicit purge"," for rare, human-triggered or deploy-time clears. ",[238,1487,1456],{}," as a routine consistency mechanism — relying on someone remembering to purge is how stale data ships to users.",[1490,1491,1493],"h3",{"id":1492},"migration-path","Migration path",[14,1495,1496],{},"Start with TTL on every cached endpoint — it's correct-enough and needs no extra code. When a specific endpoint draws staleness complaints, add write-through to just that one. Reach for tag-based invalidation only once you have a genuine fan-out problem, where one entity feeds many cached views. Layering in that order keeps complexity proportional to the consistency you need, and every piece of eviction code exists because a real incident asked for it.",[197,1498],{},[200,1500,1502],{"id":1501},"what-it-costs-to-run","What it costs to run",[14,1504,1505,1506,1509,1510,1513,1514,1517,1518,1520,1521,1524,1525,1527,1528,1532,1533,36],{},"Invalidation strategy is a margin decision as much as a correctness one, so put numbers on it. TTL adds one ",[515,1507,1508],{},"GET"," and, on a miss, one ",[515,1511,1512],{},"SET"," to the read path — two Redis commands worst case, well under a millisecond each. Write-through adds one command per mutation; if writes are one percent of traffic, that is a rounding error. Tag-based is the one to watch: ",[515,1515,1516],{},"cache_with_tag"," runs three commands per cached write, and ",[515,1519,1152],{}," runs an ",[515,1522,1523],{},"SMEMBERS"," plus a variadic ",[515,1526,1164],{},". At a million reads a month with a healthy hit rate, all of it is a few dollars of Redis compute — the caching itself saves far more by keeping requests off your database. To attribute that saving precisely, the method in ",[27,1529,1531],{"href":1530},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request"," folds hit rate straight into the per-request figure that sets your ",[27,1534,1536],{"href":1535},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002F","pricing tiers",[14,1538,1539,1540,1544],{},"The cost that actually bites is not compute, it is the human cost of a wrong strategy: a stale read that becomes a support ticket, or an over-engineered pub\u002Fsub mesh nobody can debug at 2am. Watch the real signal — hit rate and the age of served values — through your ",[27,1541,1543],{"href":1542},"\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002F","monitoring and logging"," so you upgrade a strategy on evidence rather than a hunch.",[197,1546],{},[200,1548,1550],{"id":1549},"builder-verdict","Builder verdict",[14,1552,1553],{},"Default to TTL and treat everything else as a targeted upgrade. The overwhelming majority of micro-SaaS endpoints are fine with a 60–300 second TTL plus the stampede lock from the parent guide — a handful of lines that fails safe. Add write-through the day a stale value becomes a support ticket, scope it to the offending endpoint, and prefer delete-on-write to avoid shape drift. Save tag-based invalidation and pub\u002Fsub for when you genuinely have one entity fanning out into many cached views; until then it's complexity you'll regret maintaining. The mistake to avoid is reaching for the most \"correct\" strategy first — pay for consistency only where staleness actually costs you, and let TTL carry the rest.",[197,1555],{},[200,1557,1559],{"id":1558},"faq","FAQ",[14,1561,1562,1565],{},[238,1563,1564],{},"Is TTL really enough for production?","\nFor most read-heavy endpoints, yes. A short TTL bounds staleness automatically and adds no invalidation code to maintain. Reach for active eviction only when a stale read causes a correctness problem — a revoked permission, a deleted record, a sold-out seat — not by default.",[14,1567,1568,1571],{},[238,1569,1570],{},"What's the difference between write-through and write-invalidate?","\nWrite-through rewrites the cached value during the write so the next read is instant. Write-invalidate just deletes the key and lets the next read repopulate lazily. Write-invalidate is simpler and avoids the cache drifting from your read endpoint's actual response shape, so it's the safer default. Prefer set-on-write only for a very hot key where you can't afford the post-invalidation miss.",[14,1573,1574,1577,1579,1580,1583,1584,1587],{},[238,1575,1576],{},"Why not just use Redis KEYS to find and delete related keys?",[515,1578,1173],{}," scans the entire keyspace and blocks the single-threaded Redis event loop while it runs, which can stall every request across your whole API, not just the endpoint you were purging. Maintain a tag set on write and delete its members, or use ",[515,1581,1582],{},"SCAN","\u002F",[515,1585,1586],{},"scan_iter"," for cursor-based iteration when you must walk keys.",[14,1589,1590,1593],{},[238,1591,1592],{},"How do I invalidate caches across multiple servers without hurting latency?","\nWith a single shared Redis, deleting a key once already invalidates it for every instance — you need nothing extra. Pub\u002Fsub only matters when each instance keeps its own in-process layer on top of Redis; then you publish an invalidation message on write and each instance drops the affected local keys. Don't add that machinery until you actually have a second cache layer to keep coherent.",[14,1595,1596,1599],{},[238,1597,1598],{},"Will active invalidation raise my Redis bill enough to matter?","\nNo. Write-through adds one command per mutation and tag-based adds a few per cached write; at a million reads a month that's a few dollars of compute against the far larger savings caching buys you. The expensive mistake is the wrong strategy causing stale-data tickets or an unmaintainable invalidation mesh — cost here is a correctness-and-maintenance question, not a compute one.",[197,1601],{},[200,1603,1605],{"id":1604},"related","Related",[14,1607,1608],{},[238,1609,1610],{},"Same section:",[1445,1612,1613,1619,1625],{},[1448,1614,1615,1618],{},[27,1616,1617],{"href":29},"Caching Python API Responses with Redis"," — the parent guide with the read-through pattern and stampede lock this article builds on.",[1448,1620,1621,1624],{},[27,1622,1623],{"href":1168},"Redis vs In-Memory Caching for FastAPI"," — when a per-process layer earns the pub\u002Fsub invalidation this page mentions.",[1448,1626,1627,1630],{},[27,1628,1629],{"href":1542},"Monitoring and Logging Python APIs"," — track hit rate and served-value age so you upgrade strategy on evidence.",[14,1632,1633],{},[238,1634,1635],{},"Other areas:",[1445,1637,1638,1644],{},[1448,1639,1640,1643],{},[27,1641,1642],{"href":1147},"Processing Webhooks with Python"," — turn upstream events into tag invalidations for data you don't own.",[1448,1645,1646,1649],{},[27,1647,1648],{"href":1530},"Calculating Cost per API Request"," — fold cache hit rate into the number that sets your pricing.",[1651,1652,1653],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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":513,"searchDepth":531,"depth":531,"links":1655},[1656,1657,1658,1659,1660,1663,1664,1665,1666],{"id":202,"depth":531,"text":203},{"id":400,"depth":531,"text":401},{"id":768,"depth":531,"text":769},{"id":1179,"depth":531,"text":1180},{"id":1442,"depth":531,"text":1443,"children":1661},[1662],{"id":1492,"depth":539,"text":1493},{"id":1501,"depth":531,"text":1502},{"id":1549,"depth":531,"text":1550},{"id":1558,"depth":531,"text":1559},{"id":1604,"depth":531,"text":1605},"Compare TTL, write-through, tag-based, and explicit purge invalidation for Redis caches in Python APIs — the consistency, complexity, and cost trade-offs.","md",{"pageTitle":1670,"type":1671,"datePublished":1672,"dateModified":1673},"Redis Cache Invalidation Strategies for Python Builders","article","2026-06-18","2026-07-23","\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002Fcache-invalidation-strategies",{"title":5,"description":1667},"scaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002Fcache-invalidation-strategies\u002Findex","c2nlzhqjcG4U5_pxNCohTjxJPs-naWawi3W9uIjP7eI",{"@context":1679,"@type":1680,"mainEntity":1681},"https:\u002F\u002Fschema.org","FAQPage",[1682,1687,1690,1693,1696],{"@type":1683,"name":1564,"acceptedAnswer":1684},"Question",{"@type":1685,"text":1686},"Answer","For most read-heavy endpoints, yes. A short TTL bounds staleness automatically and adds no invalidation code to maintain. Reach for active eviction only when a stale read causes a correctness problem — a revoked permission, a deleted record, a sold-out seat — not by default.",{"@type":1683,"name":1570,"acceptedAnswer":1688},{"@type":1685,"text":1689},"Write-through rewrites the cached value during the write so the next read is instant. Write-invalidate just deletes the key and lets the next read repopulate lazily. Write-invalidate is simpler and avoids the cache drifting from your read endpoint's actual response shape, so it's the safer default. Prefer set-on-write only for a very hot key where you can't afford the post-invalidation miss.",{"@type":1683,"name":1576,"acceptedAnswer":1691},{"@type":1685,"text":1692},"KEYS pattern scans the entire keyspace and blocks the single-threaded Redis event loop while it runs, which can stall every request across your whole API, not just the endpoint you were purging. Maintain a tag set on write and delete its members, or use SCAN\u002Fscan_iter for cursor-based iteration when you must walk keys.",{"@type":1683,"name":1592,"acceptedAnswer":1694},{"@type":1685,"text":1695},"With a single shared Redis, deleting a key once already invalidates it for every instance — you need nothing extra. Pub\u002Fsub only matters when each instance keeps its own in-process layer on top of Redis; then you publish an invalidation message on write and each instance drops the affected local keys. Don't add that machinery until you actually have a second cache layer to keep coherent.",{"@type":1683,"name":1598,"acceptedAnswer":1697},{"@type":1685,"text":1698},"No. Write-through adds one command per mutation and tag-based adds a few per cached write; at a million reads a month that's a few dollars of compute against the far larger savings caching buys you. The expensive mistake is the wrong strategy causing stale-data tickets or an unmaintainable invalidation mesh — cost here is a correctness-and-maintenance question, not a compute one. ---",1784887028601]