[{"data":1,"prerenderedAt":2331},["ShallowReactive",2],{"article:\u002Fuse-cases\u002Fsessions-and-rate-limiting":3,"related:\u002Fuse-cases\u002Fai-agent-memory,\u002Fcompare\u002Feu-managed-redis":2322},{"id":4,"title":5,"answer":6,"author":7,"body":8,"dateModified":2286,"datePublished":2286,"description":2287,"extension":2288,"faq":2289,"lastVerified":2302,"meta":2303,"navigation":327,"ogImage":2302,"path":2304,"pillar":2305,"providers":2306,"published":327,"related":2307,"seo":2309,"sources":2310,"stem":2320,"__hash__":2321},"useCases\u002Fuse-cases\u002Fsessions-and-rate-limiting.md","Sessions, rate limiting and stampede-safe caching on Valkey or Redis","Store each session under its own key with a TTL, rate-limit with INCR plus EXPIRE ... NX per client and window, and fill caches under a SET ... NX PX lock. On KVNest's free Starter plan, set the eviction policy to noeviction first, or the cache policy can silently drop sessions.","KVNest",{"type":9,"value":10,"toc":2272},"minimark",[11,40,61,66,82,86,91,106,269,663,667,684,1198,1202,1206,1214,1469,1479,1483,1486,1744,1747,1751,1757,2193,2199,2203,2222,2226,2255,2268],[12,13,14,31,32,35,36,39],"p",{},[15,16,17,18,22,23,26,27,30],"strong",{},"Give every session its own key and a TTL, count requests with ",[19,20,21],"code",{},"INCR"," and ",[19,24,25],{},"EXPIRE ... NX",", and guard cache refills with ",[19,28,29],{},"SET ... NX PX","."," Each is one or two commands and works the same on Valkey and on Redis. Everything below is Node.js 18 or later and reads ",[19,33,34],{},"VALKEY_URL",", the ",[19,37,38],{},"rediss:\u002F\u002F"," URL from your instance page.",[12,41,42,45,46,49,50,53,54,57,58,60],{},[15,43,44],{},"Before you store sessions on Starter:"," its eviction policy is ",[19,47,48],{},"allkeys-lru",", which deletes the least recently used keys under memory pressure, sessions included. Set it to ",[19,51,52],{},"noeviction"," on the instance's Settings tab or with the ",[19,55,56],{},"configure_instance"," tool; changing it restarts the instance. With ",[19,59,52],{},", writes fail once the 192 MB of data is full, so catch the out-of-memory error. Starter also restarts empty, because nothing is written to disk.",[62,63,65],"h2",{"id":64},"connect-once","Connect once",[12,67,68,69,74,75,22,78,81],{},"The ",[70,71,73],"a",{"href":72},"\u002Fdocs\u002Fnodejs","Node.js guide"," has the full client setup. Open one client per process, not per request. KVNest closes idle connections after five minutes, and ",[19,76,77],{},"iovalkey",[19,79,80],{},"redis"," both reconnect on their own.",[62,83,85],{"id":84},"sessions","Sessions",[87,88,90],"h3",{"id":89},"nuxt-and-nitro","Nuxt and Nitro",[12,92,93,94,97,98,101,102,105],{},"Nitro's storage layer can mount the unstorage Redis driver, which needs ",[19,95,96],{},"ioredis"," installed (",[19,99,100],{},"npm install ioredis","). The driver's ",[19,103,104],{},"ttl"," option (seconds) expires each key, and writing an item again resets it, which gives you a sliding expiry.",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-ts shiki shiki-themes kvnest-light kvnest-light kvnest-dark","\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  nitro: {\n    storage: {\n      sessions: {\n        driver: \"redis\",\n        url: process.env.VALKEY_URL,\n        base: \"sessions\",\n        ttl: 60 * 60,\n      },\n    },\n  },\n});\n","ts","",[19,114,115,124,142,155,165,175,190,213,226,245,251,257,263],{"__ignoreMap":112},[116,117,120],"span",{"class":118,"line":119},"line",1,[116,121,123],{"class":122},"sgbRl","\u002F\u002F nuxt.config.ts\n",[116,125,127,131,134,138],{"class":118,"line":126},2,[116,128,130],{"class":129},"sQ0Rz","export",[116,132,133],{"class":129}," default",[116,135,137],{"class":136},"sdT5u"," defineNuxtConfig",[116,139,141],{"class":140},"s6SZf","({\n",[116,143,145,149,152],{"class":118,"line":144},3,[116,146,148],{"class":147},"sxIm-","  nitro",[116,150,151],{"class":140},":",[116,153,154],{"class":140}," {\n",[116,156,158,161,163],{"class":118,"line":157},4,[116,159,160],{"class":147},"    storage",[116,162,151],{"class":140},[116,164,154],{"class":140},[116,166,168,171,173],{"class":118,"line":167},5,[116,169,170],{"class":147},"      sessions",[116,172,151],{"class":140},[116,174,154],{"class":140},[116,176,178,181,183,187],{"class":118,"line":177},6,[116,179,180],{"class":147},"        driver",[116,182,151],{"class":140},[116,184,186],{"class":185},"sxAeW"," \"redis\"",[116,188,189],{"class":140},",\n",[116,191,193,196,198,202,204,207,209,211],{"class":118,"line":192},7,[116,194,195],{"class":147},"        url",[116,197,151],{"class":140},[116,199,201],{"class":200},"sq_S-"," process",[116,203,30],{"class":140},[116,205,206],{"class":147},"env",[116,208,30],{"class":140},[116,210,34],{"class":200},[116,212,189],{"class":140},[116,214,216,219,221,224],{"class":118,"line":215},8,[116,217,218],{"class":147},"        base",[116,220,151],{"class":140},[116,222,223],{"class":185}," \"sessions\"",[116,225,189],{"class":140},[116,227,229,232,234,238,241,243],{"class":118,"line":228},9,[116,230,231],{"class":147},"        ttl",[116,233,151],{"class":140},[116,235,237],{"class":236},"sLNte"," 60",[116,239,240],{"class":140}," *",[116,242,237],{"class":236},[116,244,189],{"class":140},[116,246,248],{"class":118,"line":247},10,[116,249,250],{"class":140},"      },\n",[116,252,254],{"class":118,"line":253},11,[116,255,256],{"class":140},"    },\n",[116,258,260],{"class":118,"line":259},12,[116,261,262],{"class":140},"  },\n",[116,264,266],{"class":118,"line":265},13,[116,267,268],{"class":140},"});\n",[107,270,272],{"className":109,"code":271,"language":111,"meta":112,"style":112},"\u002F\u002F server\u002Futils\u002Fsession.ts\nimport { randomUUID } from \"node:crypto\";\nimport type { H3Event } from \"h3\";\n\nexport async function useAppSession(event: H3Event) {\n  const store = useStorage(\"sessions\");\n  let id = getCookie(event, \"sid\");\n  let data = id ? await store.getItem\u003CRecord\u003Cstring, unknown>>(id) : null;\n  if (!id || !data) {\n    id = randomUUID();\n    data = {};\n  }\n  setCookie(event, \"sid\", id, { httpOnly: true, secure: true, sameSite: \"lax\", maxAge: 60 * 60 });\n  return {\n    data,\n    save: () => store.setItem(id!, data),\n    destroy: () => store.removeItem(id!),\n  };\n}\n",[19,273,274,279,302,323,329,358,380,406,463,486,498,508,513,577,585,592,625,651,657],{"__ignoreMap":112},[116,275,276],{"class":118,"line":119},[116,277,278],{"class":122},"\u002F\u002F server\u002Futils\u002Fsession.ts\n",[116,280,281,284,287,290,293,296,299],{"class":118,"line":126},[116,282,283],{"class":129},"import",[116,285,286],{"class":140}," {",[116,288,289],{"class":147}," randomUUID",[116,291,292],{"class":140}," }",[116,294,295],{"class":129}," from",[116,297,298],{"class":185}," \"node:crypto\"",[116,300,301],{"class":140},";\n",[116,303,304,306,309,311,314,316,318,321],{"class":118,"line":144},[116,305,283],{"class":129},[116,307,308],{"class":129}," type",[116,310,286],{"class":140},[116,312,313],{"class":147}," H3Event",[116,315,292],{"class":140},[116,317,295],{"class":129},[116,319,320],{"class":185}," \"h3\"",[116,322,301],{"class":140},[116,324,325],{"class":118,"line":157},[116,326,328],{"emptyLinePlaceholder":327},true,"\n",[116,330,331,333,336,339,342,345,348,350,353,356],{"class":118,"line":167},[116,332,130],{"class":129},[116,334,335],{"class":129}," async",[116,337,338],{"class":129}," function",[116,340,341],{"class":136}," useAppSession",[116,343,344],{"class":140},"(",[116,346,347],{"class":147},"event",[116,349,151],{"class":140},[116,351,313],{"class":352},"sPzKH",[116,354,355],{"class":140},")",[116,357,154],{"class":140},[116,359,360,363,366,369,372,374,377],{"class":118,"line":177},[116,361,362],{"class":129},"  const",[116,364,365],{"class":200}," store ",[116,367,368],{"class":140},"=",[116,370,371],{"class":136}," useStorage",[116,373,344],{"class":140},[116,375,376],{"class":185},"\"sessions\"",[116,378,379],{"class":140},");\n",[116,381,382,385,388,391,394,396,398,401,404],{"class":118,"line":192},[116,383,384],{"class":129},"  let",[116,386,387],{"class":147}," id",[116,389,390],{"class":140}," =",[116,392,393],{"class":136}," getCookie",[116,395,344],{"class":140},[116,397,347],{"class":147},[116,399,400],{"class":140},",",[116,402,403],{"class":185}," \"sid\"",[116,405,379],{"class":140},[116,407,408,410,413,415,417,420,423,426,428,431,434,437,439,442,444,447,450,453,455,458,461],{"class":118,"line":215},[116,409,384],{"class":129},[116,411,412],{"class":147}," data",[116,414,390],{"class":140},[116,416,387],{"class":147},[116,418,419],{"class":140}," ?",[116,421,422],{"class":129}," await",[116,424,425],{"class":136}," store",[116,427,30],{"class":140},[116,429,430],{"class":136},"getItem",[116,432,433],{"class":140},"\u003C",[116,435,436],{"class":352},"Record",[116,438,433],{"class":140},[116,440,441],{"class":352},"string",[116,443,400],{"class":140},[116,445,446],{"class":352}," unknown",[116,448,449],{"class":140},">>(",[116,451,452],{"class":147},"id",[116,454,355],{"class":140},[116,456,457],{"class":140}," :",[116,459,460],{"class":236}," null",[116,462,301],{"class":140},[116,464,465,468,471,473,476,479,482,484],{"class":118,"line":228},[116,466,467],{"class":129},"  if",[116,469,470],{"class":140}," (!",[116,472,452],{"class":147},[116,474,475],{"class":140}," ||",[116,477,478],{"class":140}," !",[116,480,481],{"class":147},"data",[116,483,355],{"class":140},[116,485,154],{"class":140},[116,487,488,491,493,495],{"class":118,"line":247},[116,489,490],{"class":147},"    id",[116,492,390],{"class":140},[116,494,289],{"class":136},[116,496,497],{"class":140},"();\n",[116,499,500,503,505],{"class":118,"line":253},[116,501,502],{"class":147},"    data",[116,504,390],{"class":140},[116,506,507],{"class":140}," {};\n",[116,509,510],{"class":118,"line":259},[116,511,512],{"class":140},"  }\n",[116,514,515,518,520,522,524,526,528,530,532,534,537,539,542,544,547,549,551,553,556,558,561,563,566,568,570,572,574],{"class":118,"line":265},[116,516,517],{"class":136},"  setCookie",[116,519,344],{"class":140},[116,521,347],{"class":147},[116,523,400],{"class":140},[116,525,403],{"class":185},[116,527,400],{"class":140},[116,529,387],{"class":147},[116,531,400],{"class":140},[116,533,286],{"class":140},[116,535,536],{"class":147}," httpOnly",[116,538,151],{"class":140},[116,540,541],{"class":236}," true",[116,543,400],{"class":140},[116,545,546],{"class":147}," secure",[116,548,151],{"class":140},[116,550,541],{"class":236},[116,552,400],{"class":140},[116,554,555],{"class":147}," sameSite",[116,557,151],{"class":140},[116,559,560],{"class":185}," \"lax\"",[116,562,400],{"class":140},[116,564,565],{"class":147}," maxAge",[116,567,151],{"class":140},[116,569,237],{"class":236},[116,571,240],{"class":140},[116,573,237],{"class":236},[116,575,576],{"class":140}," });\n",[116,578,580,583],{"class":118,"line":579},14,[116,581,582],{"class":129},"  return",[116,584,154],{"class":140},[116,586,588,590],{"class":118,"line":587},15,[116,589,502],{"class":147},[116,591,189],{"class":140},[116,593,595,598,600,603,606,608,610,613,615,617,620,622],{"class":118,"line":594},16,[116,596,597],{"class":136},"    save",[116,599,151],{"class":140},[116,601,602],{"class":140}," ()",[116,604,605],{"class":129}," =>",[116,607,425],{"class":136},[116,609,30],{"class":140},[116,611,612],{"class":136},"setItem",[116,614,344],{"class":140},[116,616,452],{"class":147},[116,618,619],{"class":140},"!,",[116,621,412],{"class":147},[116,623,624],{"class":140},"),\n",[116,626,628,631,633,635,637,639,641,644,646,648],{"class":118,"line":627},17,[116,629,630],{"class":136},"    destroy",[116,632,151],{"class":140},[116,634,602],{"class":140},[116,636,605],{"class":129},[116,638,425],{"class":136},[116,640,30],{"class":140},[116,642,643],{"class":136},"removeItem",[116,645,344],{"class":140},[116,647,452],{"class":147},[116,649,650],{"class":140},"!),\n",[116,652,654],{"class":118,"line":653},18,[116,655,656],{"class":140},"  };\n",[116,658,660],{"class":118,"line":659},19,[116,661,662],{"class":140},"}\n",[87,664,666],{"id":665},"express","Express",[12,668,669,672,673,676,677,679,680,683],{},[19,670,671],{},"connect-redis"," stores each session as a JSON string under ",[19,674,675],{},"sess:\u003Cid>",". Pass ",[19,678,104],{}," so the store expires keys, and ",[19,681,682],{},"rolling: true"," to renew the cookie on each response.",[107,685,689],{"className":686,"code":687,"language":688,"meta":112,"style":112},"language-js shiki shiki-themes kvnest-light kvnest-light kvnest-dark","import express from \"express\";\nimport session from \"express-session\";\nimport { RedisStore } from \"connect-redis\";\nimport { createClient } from \"redis\";\n\nconst client = createClient({ url: process.env.VALKEY_URL });\nclient.on(\"error\", (err) => console.error(\"Redis client error\", err));\nawait client.connect();\n\nconst app = express();\napp.set(\"trust proxy\", 1); \u002F\u002F behind a proxy that terminates TLS\napp.use(session({\n  store: new RedisStore({ client, prefix: \"sess:\", ttl: 3600 }),\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  rolling: true,\n  cookie: { secure: true, httpOnly: true, sameSite: \"lax\", maxAge: 3600 * 1000 },\n}));\n\napp.get(\"\u002F\", (req, res) => {\n  req.session.views = (req.session.views ?? 0) + 1;\n  res.send(\"Views: \" + req.session.views);\n});\n\napp.listen(3000);\n","js",[19,690,691,705,719,737,754,758,791,839,854,858,871,897,913,952,972,984,995,1006,1053,1058,1063,1096,1140,1171,1176,1181],{"__ignoreMap":112},[116,692,693,695,698,700,703],{"class":118,"line":119},[116,694,283],{"class":129},[116,696,697],{"class":147}," express",[116,699,295],{"class":129},[116,701,702],{"class":185}," \"express\"",[116,704,301],{"class":140},[116,706,707,709,712,714,717],{"class":118,"line":126},[116,708,283],{"class":129},[116,710,711],{"class":147}," session",[116,713,295],{"class":129},[116,715,716],{"class":185}," \"express-session\"",[116,718,301],{"class":140},[116,720,721,723,725,728,730,732,735],{"class":118,"line":144},[116,722,283],{"class":129},[116,724,286],{"class":140},[116,726,727],{"class":147}," RedisStore",[116,729,292],{"class":140},[116,731,295],{"class":129},[116,733,734],{"class":185}," \"connect-redis\"",[116,736,301],{"class":140},[116,738,739,741,743,746,748,750,752],{"class":118,"line":157},[116,740,283],{"class":129},[116,742,286],{"class":140},[116,744,745],{"class":147}," createClient",[116,747,292],{"class":140},[116,749,295],{"class":129},[116,751,186],{"class":185},[116,753,301],{"class":140},[116,755,756],{"class":118,"line":167},[116,757,328],{"emptyLinePlaceholder":327},[116,759,760,763,766,768,770,773,776,778,780,782,784,786,789],{"class":118,"line":177},[116,761,762],{"class":129},"const",[116,764,765],{"class":200}," client ",[116,767,368],{"class":140},[116,769,745],{"class":136},[116,771,772],{"class":140},"({",[116,774,775],{"class":147}," url",[116,777,151],{"class":140},[116,779,201],{"class":200},[116,781,30],{"class":140},[116,783,206],{"class":147},[116,785,30],{"class":140},[116,787,788],{"class":200},"VALKEY_URL ",[116,790,268],{"class":140},[116,792,793,796,798,801,803,806,808,811,814,816,818,821,823,826,828,831,833,836],{"class":118,"line":192},[116,794,795],{"class":136},"client",[116,797,30],{"class":140},[116,799,800],{"class":136},"on",[116,802,344],{"class":140},[116,804,805],{"class":185},"\"error\"",[116,807,400],{"class":140},[116,809,810],{"class":140}," (",[116,812,813],{"class":147},"err",[116,815,355],{"class":140},[116,817,605],{"class":129},[116,819,820],{"class":136}," console",[116,822,30],{"class":140},[116,824,825],{"class":136},"error",[116,827,344],{"class":140},[116,829,830],{"class":185},"\"Redis client error\"",[116,832,400],{"class":140},[116,834,835],{"class":147}," err",[116,837,838],{"class":140},"));\n",[116,840,841,844,847,849,852],{"class":118,"line":215},[116,842,843],{"class":129},"await",[116,845,846],{"class":136}," client",[116,848,30],{"class":140},[116,850,851],{"class":136},"connect",[116,853,497],{"class":140},[116,855,856],{"class":118,"line":228},[116,857,328],{"emptyLinePlaceholder":327},[116,859,860,862,865,867,869],{"class":118,"line":247},[116,861,762],{"class":129},[116,863,864],{"class":200}," app ",[116,866,368],{"class":140},[116,868,697],{"class":136},[116,870,497],{"class":140},[116,872,873,876,878,881,883,886,888,891,894],{"class":118,"line":253},[116,874,875],{"class":136},"app",[116,877,30],{"class":140},[116,879,880],{"class":136},"set",[116,882,344],{"class":140},[116,884,885],{"class":185},"\"trust proxy\"",[116,887,400],{"class":140},[116,889,890],{"class":236}," 1",[116,892,893],{"class":140},");",[116,895,896],{"class":122}," \u002F\u002F behind a proxy that terminates TLS\n",[116,898,899,901,903,906,908,911],{"class":118,"line":259},[116,900,875],{"class":136},[116,902,30],{"class":140},[116,904,905],{"class":136},"use",[116,907,344],{"class":140},[116,909,910],{"class":136},"session",[116,912,141],{"class":140},[116,914,915,918,920,923,925,927,929,931,934,936,939,941,944,946,949],{"class":118,"line":265},[116,916,917],{"class":147},"  store",[116,919,151],{"class":140},[116,921,922],{"class":129}," new",[116,924,727],{"class":136},[116,926,772],{"class":140},[116,928,846],{"class":147},[116,930,400],{"class":140},[116,932,933],{"class":147}," prefix",[116,935,151],{"class":140},[116,937,938],{"class":185}," \"sess:\"",[116,940,400],{"class":140},[116,942,943],{"class":147}," ttl",[116,945,151],{"class":140},[116,947,948],{"class":236}," 3600",[116,950,951],{"class":140}," }),\n",[116,953,954,957,959,961,963,965,967,970],{"class":118,"line":579},[116,955,956],{"class":147},"  secret",[116,958,151],{"class":140},[116,960,201],{"class":200},[116,962,30],{"class":140},[116,964,206],{"class":147},[116,966,30],{"class":140},[116,968,969],{"class":200},"SESSION_SECRET",[116,971,189],{"class":140},[116,973,974,977,979,982],{"class":118,"line":587},[116,975,976],{"class":147},"  resave",[116,978,151],{"class":140},[116,980,981],{"class":236}," false",[116,983,189],{"class":140},[116,985,986,989,991,993],{"class":118,"line":594},[116,987,988],{"class":147},"  saveUninitialized",[116,990,151],{"class":140},[116,992,981],{"class":236},[116,994,189],{"class":140},[116,996,997,1000,1002,1004],{"class":118,"line":627},[116,998,999],{"class":147},"  rolling",[116,1001,151],{"class":140},[116,1003,541],{"class":236},[116,1005,189],{"class":140},[116,1007,1008,1011,1013,1015,1017,1019,1021,1023,1025,1027,1029,1031,1033,1035,1037,1039,1041,1043,1045,1047,1050],{"class":118,"line":653},[116,1009,1010],{"class":147},"  cookie",[116,1012,151],{"class":140},[116,1014,286],{"class":140},[116,1016,546],{"class":147},[116,1018,151],{"class":140},[116,1020,541],{"class":236},[116,1022,400],{"class":140},[116,1024,536],{"class":147},[116,1026,151],{"class":140},[116,1028,541],{"class":236},[116,1030,400],{"class":140},[116,1032,555],{"class":147},[116,1034,151],{"class":140},[116,1036,560],{"class":185},[116,1038,400],{"class":140},[116,1040,565],{"class":147},[116,1042,151],{"class":140},[116,1044,948],{"class":236},[116,1046,240],{"class":140},[116,1048,1049],{"class":236}," 1000",[116,1051,1052],{"class":140}," },\n",[116,1054,1055],{"class":118,"line":659},[116,1056,1057],{"class":140},"}));\n",[116,1059,1061],{"class":118,"line":1060},20,[116,1062,328],{"emptyLinePlaceholder":327},[116,1064,1066,1068,1070,1073,1075,1078,1080,1082,1085,1087,1090,1092,1094],{"class":118,"line":1065},21,[116,1067,875],{"class":136},[116,1069,30],{"class":140},[116,1071,1072],{"class":136},"get",[116,1074,344],{"class":140},[116,1076,1077],{"class":185},"\"\u002F\"",[116,1079,400],{"class":140},[116,1081,810],{"class":140},[116,1083,1084],{"class":147},"req",[116,1086,400],{"class":140},[116,1088,1089],{"class":147}," res",[116,1091,355],{"class":140},[116,1093,605],{"class":129},[116,1095,154],{"class":140},[116,1097,1099,1102,1104,1106,1108,1111,1113,1115,1117,1119,1121,1123,1125,1128,1131,1133,1136,1138],{"class":118,"line":1098},22,[116,1100,1101],{"class":200},"  req",[116,1103,30],{"class":140},[116,1105,910],{"class":147},[116,1107,30],{"class":140},[116,1109,1110],{"class":147},"views",[116,1112,390],{"class":140},[116,1114,810],{"class":140},[116,1116,1084],{"class":200},[116,1118,30],{"class":140},[116,1120,910],{"class":147},[116,1122,30],{"class":140},[116,1124,1110],{"class":147},[116,1126,1127],{"class":140}," ??",[116,1129,1130],{"class":236}," 0",[116,1132,355],{"class":140},[116,1134,1135],{"class":140}," +",[116,1137,890],{"class":236},[116,1139,301],{"class":140},[116,1141,1143,1146,1148,1151,1153,1156,1158,1161,1163,1165,1167,1169],{"class":118,"line":1142},23,[116,1144,1145],{"class":136},"  res",[116,1147,30],{"class":140},[116,1149,1150],{"class":136},"send",[116,1152,344],{"class":140},[116,1154,1155],{"class":185},"\"Views: \"",[116,1157,1135],{"class":140},[116,1159,1160],{"class":200}," req",[116,1162,30],{"class":140},[116,1164,910],{"class":147},[116,1166,30],{"class":140},[116,1168,1110],{"class":147},[116,1170,379],{"class":140},[116,1172,1174],{"class":118,"line":1173},24,[116,1175,268],{"class":140},[116,1177,1179],{"class":118,"line":1178},25,[116,1180,328],{"emptyLinePlaceholder":327},[116,1182,1184,1186,1188,1191,1193,1196],{"class":118,"line":1183},26,[116,1185,875],{"class":136},[116,1187,30],{"class":140},[116,1189,1190],{"class":136},"listen",[116,1192,344],{"class":140},[116,1194,1195],{"class":236},"3000",[116,1197,379],{"class":140},[62,1199,1201],{"id":1200},"rate-limiting","Rate limiting",[87,1203,1205],{"id":1204},"fixed-window","Fixed window",[12,1207,1208,1210,1211,1213],{},[19,1209,21],{}," creates the key at 1 on the first request. ",[19,1212,25],{}," sets the TTL only when none exists, so later requests do not extend the window. Both run in one transaction.",[107,1215,1217],{"className":686,"code":1216,"language":688,"meta":112,"style":112},"import Valkey from \"iovalkey\";\n\nconst client = new Valkey(process.env.VALKEY_URL);\n\nexport async function fixedWindow(id, limit, windowSec) {\n  const key = `rl:${id}`;\n  const results = await client.multi().incr(key).expire(key, windowSec, \"NX\").exec();\n  const count = results[0][1];\n  return { allowed: count \u003C= limit, remaining: Math.max(0, limit - count) };\n}\n\n\u002F\u002F await fixedWindow(`login:${ip}`, 5, 60)\n",[19,1218,1219,1233,1237,1264,1268,1297,1322,1377,1404,1456,1460,1464],{"__ignoreMap":112},[116,1220,1221,1223,1226,1228,1231],{"class":118,"line":119},[116,1222,283],{"class":129},[116,1224,1225],{"class":147}," Valkey",[116,1227,295],{"class":129},[116,1229,1230],{"class":185}," \"iovalkey\"",[116,1232,301],{"class":140},[116,1234,1235],{"class":118,"line":126},[116,1236,328],{"emptyLinePlaceholder":327},[116,1238,1239,1241,1243,1245,1247,1249,1251,1254,1256,1258,1260,1262],{"class":118,"line":144},[116,1240,762],{"class":129},[116,1242,765],{"class":200},[116,1244,368],{"class":140},[116,1246,922],{"class":129},[116,1248,1225],{"class":136},[116,1250,344],{"class":140},[116,1252,1253],{"class":200},"process",[116,1255,30],{"class":140},[116,1257,206],{"class":147},[116,1259,30],{"class":140},[116,1261,34],{"class":200},[116,1263,379],{"class":140},[116,1265,1266],{"class":118,"line":157},[116,1267,328],{"emptyLinePlaceholder":327},[116,1269,1270,1272,1274,1276,1279,1281,1283,1285,1288,1290,1293,1295],{"class":118,"line":167},[116,1271,130],{"class":129},[116,1273,335],{"class":129},[116,1275,338],{"class":129},[116,1277,1278],{"class":136}," fixedWindow",[116,1280,344],{"class":140},[116,1282,452],{"class":147},[116,1284,400],{"class":140},[116,1286,1287],{"class":147}," limit",[116,1289,400],{"class":140},[116,1291,1292],{"class":147}," windowSec",[116,1294,355],{"class":140},[116,1296,154],{"class":140},[116,1298,1299,1301,1304,1306,1309,1312,1314,1317,1320],{"class":118,"line":177},[116,1300,362],{"class":129},[116,1302,1303],{"class":200}," key ",[116,1305,368],{"class":140},[116,1307,1308],{"class":185}," `rl:",[116,1310,1311],{"class":140},"${",[116,1313,452],{"class":147},[116,1315,1316],{"class":140},"}",[116,1318,1319],{"class":185},"`",[116,1321,301],{"class":140},[116,1323,1324,1326,1329,1331,1333,1335,1337,1340,1343,1346,1348,1351,1354,1357,1359,1361,1363,1365,1367,1370,1372,1375],{"class":118,"line":192},[116,1325,362],{"class":129},[116,1327,1328],{"class":200}," results ",[116,1330,368],{"class":140},[116,1332,422],{"class":129},[116,1334,846],{"class":136},[116,1336,30],{"class":140},[116,1338,1339],{"class":136},"multi",[116,1341,1342],{"class":140},"().",[116,1344,1345],{"class":136},"incr",[116,1347,344],{"class":140},[116,1349,1350],{"class":147},"key",[116,1352,1353],{"class":140},").",[116,1355,1356],{"class":136},"expire",[116,1358,344],{"class":140},[116,1360,1350],{"class":147},[116,1362,400],{"class":140},[116,1364,1292],{"class":147},[116,1366,400],{"class":140},[116,1368,1369],{"class":185}," \"NX\"",[116,1371,1353],{"class":140},[116,1373,1374],{"class":136},"exec",[116,1376,497],{"class":140},[116,1378,1379,1381,1384,1386,1389,1392,1395,1398,1401],{"class":118,"line":215},[116,1380,362],{"class":129},[116,1382,1383],{"class":200}," count ",[116,1385,368],{"class":140},[116,1387,1388],{"class":147}," results",[116,1390,1391],{"class":140},"[",[116,1393,1394],{"class":236},"0",[116,1396,1397],{"class":140},"][",[116,1399,1400],{"class":236},"1",[116,1402,1403],{"class":140},"];\n",[116,1405,1406,1408,1410,1413,1415,1418,1421,1423,1425,1428,1430,1433,1435,1438,1440,1442,1444,1446,1449,1451,1453],{"class":118,"line":228},[116,1407,582],{"class":129},[116,1409,286],{"class":140},[116,1411,1412],{"class":147}," allowed",[116,1414,151],{"class":140},[116,1416,1417],{"class":147}," count",[116,1419,1420],{"class":140}," \u003C=",[116,1422,1287],{"class":147},[116,1424,400],{"class":140},[116,1426,1427],{"class":147}," remaining",[116,1429,151],{"class":140},[116,1431,1432],{"class":136}," Math",[116,1434,30],{"class":140},[116,1436,1437],{"class":136},"max",[116,1439,344],{"class":140},[116,1441,1394],{"class":236},[116,1443,400],{"class":140},[116,1445,1287],{"class":147},[116,1447,1448],{"class":140}," -",[116,1450,1417],{"class":147},[116,1452,355],{"class":140},[116,1454,1455],{"class":140}," };\n",[116,1457,1458],{"class":118,"line":247},[116,1459,662],{"class":140},[116,1461,1462],{"class":118,"line":253},[116,1463,328],{"emptyLinePlaceholder":327},[116,1465,1466],{"class":118,"line":259},[116,1467,1468],{"class":122},"\u002F\u002F await fixedWindow(`login:${ip}`, 5, 60)\n",[12,1470,1471,1472,1475,1476,1478],{},"A client can send ",[19,1473,1474],{},"limit"," requests at the end of one window and ",[19,1477,1474],{}," more at the start of the next.",[87,1480,1482],{"id":1481},"sliding-window","Sliding window",[12,1484,1485],{},"A sorted set holds one member per request, scored by timestamp. Trim old entries, add this request, count. Rejected requests still count, so a client that keeps hammering stays blocked.",[107,1487,1489],{"className":686,"code":1488,"language":688,"meta":112,"style":112},"import { randomUUID } from \"node:crypto\";\n\nexport async function slidingWindow(id, limit, windowMs) {\n  const key = `rl:sw:${id}`;\n  const now = Date.now();\n  const results = await client\n    .multi()\n    .zremrangebyscore(key, 0, now - windowMs)\n    .zadd(key, now, `${now}-${randomUUID()}`)\n    .zcard(key)\n    .pexpire(key, windowMs)\n    .exec();\n  return { allowed: results[2][1] \u003C= limit };\n}\n",[19,1490,1491,1507,1511,1539,1560,1579,1592,1602,1629,1672,1685,1702,1710,1740],{"__ignoreMap":112},[116,1492,1493,1495,1497,1499,1501,1503,1505],{"class":118,"line":119},[116,1494,283],{"class":129},[116,1496,286],{"class":140},[116,1498,289],{"class":147},[116,1500,292],{"class":140},[116,1502,295],{"class":129},[116,1504,298],{"class":185},[116,1506,301],{"class":140},[116,1508,1509],{"class":118,"line":126},[116,1510,328],{"emptyLinePlaceholder":327},[116,1512,1513,1515,1517,1519,1522,1524,1526,1528,1530,1532,1535,1537],{"class":118,"line":144},[116,1514,130],{"class":129},[116,1516,335],{"class":129},[116,1518,338],{"class":129},[116,1520,1521],{"class":136}," slidingWindow",[116,1523,344],{"class":140},[116,1525,452],{"class":147},[116,1527,400],{"class":140},[116,1529,1287],{"class":147},[116,1531,400],{"class":140},[116,1533,1534],{"class":147}," windowMs",[116,1536,355],{"class":140},[116,1538,154],{"class":140},[116,1540,1541,1543,1545,1547,1550,1552,1554,1556,1558],{"class":118,"line":157},[116,1542,362],{"class":129},[116,1544,1303],{"class":200},[116,1546,368],{"class":140},[116,1548,1549],{"class":185}," `rl:sw:",[116,1551,1311],{"class":140},[116,1553,452],{"class":147},[116,1555,1316],{"class":140},[116,1557,1319],{"class":185},[116,1559,301],{"class":140},[116,1561,1562,1564,1567,1569,1572,1574,1577],{"class":118,"line":167},[116,1563,362],{"class":129},[116,1565,1566],{"class":200}," now ",[116,1568,368],{"class":140},[116,1570,1571],{"class":136}," Date",[116,1573,30],{"class":140},[116,1575,1576],{"class":136},"now",[116,1578,497],{"class":140},[116,1580,1581,1583,1585,1587,1589],{"class":118,"line":177},[116,1582,362],{"class":129},[116,1584,1328],{"class":200},[116,1586,368],{"class":140},[116,1588,422],{"class":129},[116,1590,1591],{"class":147}," client\n",[116,1593,1594,1597,1599],{"class":118,"line":192},[116,1595,1596],{"class":140},"    .",[116,1598,1339],{"class":136},[116,1600,1601],{"class":140},"()\n",[116,1603,1604,1606,1609,1611,1613,1615,1617,1619,1622,1624,1626],{"class":118,"line":215},[116,1605,1596],{"class":140},[116,1607,1608],{"class":136},"zremrangebyscore",[116,1610,344],{"class":140},[116,1612,1350],{"class":147},[116,1614,400],{"class":140},[116,1616,1130],{"class":236},[116,1618,400],{"class":140},[116,1620,1621],{"class":147}," now",[116,1623,1448],{"class":140},[116,1625,1534],{"class":147},[116,1627,1628],{"class":140},")\n",[116,1630,1631,1633,1636,1638,1640,1642,1644,1646,1649,1651,1653,1655,1658,1660,1663,1666,1668,1670],{"class":118,"line":228},[116,1632,1596],{"class":140},[116,1634,1635],{"class":136},"zadd",[116,1637,344],{"class":140},[116,1639,1350],{"class":147},[116,1641,400],{"class":140},[116,1643,1621],{"class":147},[116,1645,400],{"class":140},[116,1647,1648],{"class":185}," `",[116,1650,1311],{"class":140},[116,1652,1576],{"class":147},[116,1654,1316],{"class":140},[116,1656,1657],{"class":185},"-",[116,1659,1311],{"class":140},[116,1661,1662],{"class":136},"randomUUID",[116,1664,1665],{"class":140},"()",[116,1667,1316],{"class":140},[116,1669,1319],{"class":185},[116,1671,1628],{"class":140},[116,1673,1674,1676,1679,1681,1683],{"class":118,"line":247},[116,1675,1596],{"class":140},[116,1677,1678],{"class":136},"zcard",[116,1680,344],{"class":140},[116,1682,1350],{"class":147},[116,1684,1628],{"class":140},[116,1686,1687,1689,1692,1694,1696,1698,1700],{"class":118,"line":253},[116,1688,1596],{"class":140},[116,1690,1691],{"class":136},"pexpire",[116,1693,344],{"class":140},[116,1695,1350],{"class":147},[116,1697,400],{"class":140},[116,1699,1534],{"class":147},[116,1701,1628],{"class":140},[116,1703,1704,1706,1708],{"class":118,"line":259},[116,1705,1596],{"class":140},[116,1707,1374],{"class":136},[116,1709,497],{"class":140},[116,1711,1712,1714,1716,1718,1720,1722,1724,1727,1729,1731,1734,1736,1738],{"class":118,"line":265},[116,1713,582],{"class":129},[116,1715,286],{"class":140},[116,1717,1412],{"class":147},[116,1719,151],{"class":140},[116,1721,1388],{"class":147},[116,1723,1391],{"class":140},[116,1725,1726],{"class":236},"2",[116,1728,1397],{"class":140},[116,1730,1400],{"class":236},[116,1732,1733],{"class":140},"]",[116,1735,1420],{"class":140},[116,1737,1287],{"class":147},[116,1739,1455],{"class":140},[116,1741,1742],{"class":118,"line":579},[116,1743,662],{"class":140},[12,1745,1746],{},"Memory is one entry per request in the window, so use it for low limits such as login attempts, not for 10,000 requests a minute.",[62,1748,1750],{"id":1749},"stampede-safe-caching","Stampede-safe caching",[12,1752,1753,1754,1756],{},"When a hot key expires, many requests recompute it at once. Let one caller take a lock with ",[19,1755,29],{},"; the others poll. The lock expires by itself if its owner crashes, and a token keeps one caller from releasing another's lock.",[107,1758,1760],{"className":686,"code":1759,"language":688,"meta":112,"style":112},"const sleep = (ms) => new Promise((r) => setTimeout(r, ms));\nconst unlock = `if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"del\", KEYS[1]) end return 0`;\n\nexport async function cached(key, ttlMs, compute) {\n  for (let attempt = 0; attempt \u003C 50; attempt++) {\n    const hit = await client.get(key);\n    if (hit !== null) return JSON.parse(hit);\n    const token = randomUUID();\n    if (await client.set(`lock:${key}`, token, \"PX\", 10_000, \"NX\") === \"OK\") {\n      try {\n        const value = await compute();\n        await client.set(key, JSON.stringify(value), \"PX\", ttlMs);\n        return value;\n      } finally {\n        await client.eval(unlock, 1, `lock:${key}`, token);\n      }\n    }\n    await sleep(100);\n  }\n  throw new Error(\"cache fill timed out\");\n}\n",[19,1761,1762,1809,1823,1827,1856,1893,1917,1951,1964,2022,2029,2045,2085,2095,2105,2144,2149,2154,2168,2172,2189],{"__ignoreMap":112},[116,1763,1764,1766,1769,1771,1773,1776,1778,1780,1782,1785,1788,1791,1793,1795,1798,1800,1802,1804,1807],{"class":118,"line":119},[116,1765,762],{"class":129},[116,1767,1768],{"class":136}," sleep",[116,1770,390],{"class":140},[116,1772,810],{"class":140},[116,1774,1775],{"class":147},"ms",[116,1777,355],{"class":140},[116,1779,605],{"class":129},[116,1781,922],{"class":129},[116,1783,1784],{"class":352}," Promise",[116,1786,1787],{"class":140},"((",[116,1789,1790],{"class":147},"r",[116,1792,355],{"class":140},[116,1794,605],{"class":129},[116,1796,1797],{"class":136}," setTimeout",[116,1799,344],{"class":140},[116,1801,1790],{"class":147},[116,1803,400],{"class":140},[116,1805,1806],{"class":147}," ms",[116,1808,838],{"class":140},[116,1810,1811,1813,1816,1818,1821],{"class":118,"line":126},[116,1812,762],{"class":129},[116,1814,1815],{"class":200}," unlock ",[116,1817,368],{"class":140},[116,1819,1820],{"class":185}," `if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"del\", KEYS[1]) end return 0`",[116,1822,301],{"class":140},[116,1824,1825],{"class":118,"line":144},[116,1826,328],{"emptyLinePlaceholder":327},[116,1828,1829,1831,1833,1835,1838,1840,1842,1844,1847,1849,1852,1854],{"class":118,"line":157},[116,1830,130],{"class":129},[116,1832,335],{"class":129},[116,1834,338],{"class":129},[116,1836,1837],{"class":136}," cached",[116,1839,344],{"class":140},[116,1841,1350],{"class":147},[116,1843,400],{"class":140},[116,1845,1846],{"class":147}," ttlMs",[116,1848,400],{"class":140},[116,1850,1851],{"class":147}," compute",[116,1853,355],{"class":140},[116,1855,154],{"class":140},[116,1857,1858,1861,1863,1866,1869,1871,1873,1876,1878,1881,1884,1886,1888,1891],{"class":118,"line":167},[116,1859,1860],{"class":129},"  for",[116,1862,810],{"class":140},[116,1864,1865],{"class":129},"let",[116,1867,1868],{"class":147}," attempt",[116,1870,390],{"class":140},[116,1872,1130],{"class":236},[116,1874,1875],{"class":140},";",[116,1877,1868],{"class":147},[116,1879,1880],{"class":140}," \u003C",[116,1882,1883],{"class":236}," 50",[116,1885,1875],{"class":140},[116,1887,1868],{"class":147},[116,1889,1890],{"class":140},"++)",[116,1892,154],{"class":140},[116,1894,1895,1898,1901,1903,1905,1907,1909,1911,1913,1915],{"class":118,"line":177},[116,1896,1897],{"class":129},"    const",[116,1899,1900],{"class":200}," hit ",[116,1902,368],{"class":140},[116,1904,422],{"class":129},[116,1906,846],{"class":136},[116,1908,30],{"class":140},[116,1910,1072],{"class":136},[116,1912,344],{"class":140},[116,1914,1350],{"class":147},[116,1916,379],{"class":140},[116,1918,1919,1922,1924,1927,1930,1932,1934,1937,1940,1942,1945,1947,1949],{"class":118,"line":192},[116,1920,1921],{"class":129},"    if",[116,1923,810],{"class":140},[116,1925,1926],{"class":147},"hit",[116,1928,1929],{"class":140}," !==",[116,1931,460],{"class":236},[116,1933,355],{"class":140},[116,1935,1936],{"class":129}," return",[116,1938,1939],{"class":136}," JSON",[116,1941,30],{"class":140},[116,1943,1944],{"class":136},"parse",[116,1946,344],{"class":140},[116,1948,1926],{"class":147},[116,1950,379],{"class":140},[116,1952,1953,1955,1958,1960,1962],{"class":118,"line":215},[116,1954,1897],{"class":129},[116,1956,1957],{"class":200}," token ",[116,1959,368],{"class":140},[116,1961,289],{"class":136},[116,1963,497],{"class":140},[116,1965,1966,1968,1970,1972,1974,1976,1978,1980,1983,1985,1987,1989,1991,1993,1996,1998,2001,2003,2006,2008,2010,2012,2015,2018,2020],{"class":118,"line":228},[116,1967,1921],{"class":129},[116,1969,810],{"class":140},[116,1971,843],{"class":129},[116,1973,846],{"class":136},[116,1975,30],{"class":140},[116,1977,880],{"class":136},[116,1979,344],{"class":140},[116,1981,1982],{"class":185},"`lock:",[116,1984,1311],{"class":140},[116,1986,1350],{"class":147},[116,1988,1316],{"class":140},[116,1990,1319],{"class":185},[116,1992,400],{"class":140},[116,1994,1995],{"class":147}," token",[116,1997,400],{"class":140},[116,1999,2000],{"class":185}," \"PX\"",[116,2002,400],{"class":140},[116,2004,2005],{"class":236}," 10_000",[116,2007,400],{"class":140},[116,2009,1369],{"class":185},[116,2011,355],{"class":140},[116,2013,2014],{"class":140}," ===",[116,2016,2017],{"class":185}," \"OK\"",[116,2019,355],{"class":140},[116,2021,154],{"class":140},[116,2023,2024,2027],{"class":118,"line":247},[116,2025,2026],{"class":129},"      try",[116,2028,154],{"class":140},[116,2030,2031,2034,2037,2039,2041,2043],{"class":118,"line":253},[116,2032,2033],{"class":129},"        const",[116,2035,2036],{"class":200}," value ",[116,2038,368],{"class":140},[116,2040,422],{"class":129},[116,2042,1851],{"class":136},[116,2044,497],{"class":140},[116,2046,2047,2050,2052,2054,2056,2058,2060,2062,2064,2066,2069,2071,2074,2077,2079,2081,2083],{"class":118,"line":259},[116,2048,2049],{"class":129},"        await",[116,2051,846],{"class":136},[116,2053,30],{"class":140},[116,2055,880],{"class":136},[116,2057,344],{"class":140},[116,2059,1350],{"class":147},[116,2061,400],{"class":140},[116,2063,1939],{"class":136},[116,2065,30],{"class":140},[116,2067,2068],{"class":136},"stringify",[116,2070,344],{"class":140},[116,2072,2073],{"class":147},"value",[116,2075,2076],{"class":140},"),",[116,2078,2000],{"class":185},[116,2080,400],{"class":140},[116,2082,1846],{"class":147},[116,2084,379],{"class":140},[116,2086,2087,2090,2093],{"class":118,"line":265},[116,2088,2089],{"class":129},"        return",[116,2091,2092],{"class":147}," value",[116,2094,301],{"class":140},[116,2096,2097,2100,2103],{"class":118,"line":579},[116,2098,2099],{"class":140},"      }",[116,2101,2102],{"class":129}," finally",[116,2104,154],{"class":140},[116,2106,2107,2109,2111,2113,2116,2118,2121,2123,2125,2127,2130,2132,2134,2136,2138,2140,2142],{"class":118,"line":587},[116,2108,2049],{"class":129},[116,2110,846],{"class":136},[116,2112,30],{"class":140},[116,2114,2115],{"class":136},"eval",[116,2117,344],{"class":140},[116,2119,2120],{"class":147},"unlock",[116,2122,400],{"class":140},[116,2124,890],{"class":236},[116,2126,400],{"class":140},[116,2128,2129],{"class":185}," `lock:",[116,2131,1311],{"class":140},[116,2133,1350],{"class":147},[116,2135,1316],{"class":140},[116,2137,1319],{"class":185},[116,2139,400],{"class":140},[116,2141,1995],{"class":147},[116,2143,379],{"class":140},[116,2145,2146],{"class":118,"line":594},[116,2147,2148],{"class":140},"      }\n",[116,2150,2151],{"class":118,"line":627},[116,2152,2153],{"class":140},"    }\n",[116,2155,2156,2159,2161,2163,2166],{"class":118,"line":653},[116,2157,2158],{"class":129},"    await",[116,2160,1768],{"class":136},[116,2162,344],{"class":140},[116,2164,2165],{"class":236},"100",[116,2167,379],{"class":140},[116,2169,2170],{"class":118,"line":659},[116,2171,512],{"class":140},[116,2173,2174,2177,2179,2182,2184,2187],{"class":118,"line":1060},[116,2175,2176],{"class":129},"  throw",[116,2178,922],{"class":129},[116,2180,2181],{"class":136}," Error",[116,2183,344],{"class":140},[116,2185,2186],{"class":185},"\"cache fill timed out\"",[116,2188,379],{"class":140},[116,2190,2191],{"class":118,"line":1065},[116,2192,662],{"class":140},[12,2194,2195,2196,2198],{},"A cache can use Starter's default ",[19,2197,48],{},". A shared instance for sessions and caches cannot have both policies, so use one instance for each.",[62,2200,2202],{"id":2201},"lock-it-down","Lock it down",[12,2204,2205,2206,2209,2210,22,2213,2216,2217,2221],{},"Create a read-write user limited to the key patterns ",[19,2207,2208],{},"sess:*",", ",[19,2211,2212],{},"sessions:*",[19,2214,2215],{},"rl:*",", and give the app that user's URL instead of the default one. If your host has fixed egress IPs, add them as an IP allowlist. Both are on the instance page or in the ",[70,2218,2220],{"href":2219},"\u002Fdocs\u002Fmcp","MCP tools",", where an agent such as Claude Code can create the instance for you.",[62,2223,2225],{"id":2224},"where-kvnest-falls-short","Where KVNest falls short",[2227,2228,2230,2252],"limits-box",{"title":2229},"Limits",[2231,2232,2233,2240,2246],"ul",{},[2234,2235,2236,2239],"li",{},[15,2237,2238],{},"No persistence on Starter."," A restart or maintenance logs everyone out; there are no backups.",[2234,2241,2242,2245],{},[15,2243,2244],{},"Single node."," No failover, and no SLA during the preview.",[2234,2247,2248,2251],{},[15,2249,2250],{},"100 clients on Starter."," Serverless functions that open a connection per invocation can run out; reuse the client.",[12,2253,2254],{},"For sessions you cannot lose, wait for a persistent plan.",[12,2256,2257,2258,2262,2263,2267],{},"Starter is free during the preview, with 256 MiB of memory, and 192 MB of that is usable. It runs on Hetzner in Germany. See ",[70,2259,2261],{"href":2260},"\u002Fpricing","pricing"," and the ",[70,2264,2266],{"href":2265},"\u002Fcompare\u002Feu-managed-redis","EU managed Redis and Valkey comparison"," for the alternatives.",[2269,2270,2271],"style",{},"html pre.shiki code .sgbRl, html code.shiki .sgbRl{--shiki-light:#425674;--shiki-default:#425674;--shiki-dark:#819DCD}html pre.shiki code .sQ0Rz, html code.shiki .sQ0Rz{--shiki-light:#1A32EC;--shiki-default:#1A32EC;--shiki-dark:#83A2FF}html pre.shiki code .sdT5u, html code.shiki .sdT5u{--shiki-light:#005F78;--shiki-default:#005F78;--shiki-dark:#63D6F1}html pre.shiki code .s6SZf, html code.shiki .s6SZf{--shiki-light:#425674;--shiki-default:#425674;--shiki-dark:#9CAFCE}html pre.shiki code .sxIm-, html code.shiki .sxIm-{--shiki-light:#1D293C;--shiki-default:#1D293C;--shiki-dark:#DBE9FF}html pre.shiki code .sxAeW, html code.shiki .sxAeW{--shiki-light:#973C00;--shiki-default:#973C00;--shiki-dark:#FFB900}html pre.shiki code .sq_S-, html code.shiki .sq_S-{--shiki-light:#0E1625;--shiki-default:#0E1625;--shiki-dark:#CCE0FF}html pre.shiki code .sLNte, html code.shiki .sLNte{--shiki-light:#132BCE;--shiki-default:#132BCE;--shiki-dark:#A1BAFF}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);}html pre.shiki code .sPzKH, html code.shiki .sPzKH{--shiki-light:#104E64;--shiki-default:#104E64;--shiki-dark:#8CE5E4}",{"title":112,"searchDepth":126,"depth":144,"links":2273},[2274,2275,2279,2283,2284,2285],{"id":64,"depth":126,"text":65},{"id":84,"depth":126,"text":85,"children":2276},[2277,2278],{"id":89,"depth":144,"text":90},{"id":665,"depth":144,"text":666},{"id":1200,"depth":126,"text":1201,"children":2280},[2281,2282],{"id":1204,"depth":144,"text":1205},{"id":1481,"depth":144,"text":1482},{"id":1749,"depth":126,"text":1750},{"id":2201,"depth":126,"text":2202},{"id":2224,"depth":126,"text":2225},"2026-11-05","Keep sessions with a TTL, limit requests with INCR and EXPIRE, and cache without stampedes. Runnable Node.js code for Nuxt and Express on KVNest.","md",[2290,2293,2296,2299],{"question":2291,"answer":2292},"Can I store sessions on a free managed Redis or Valkey instance?","Yes, with two caveats on KVNest Starter. Switch the eviction policy from allkeys-lru to noeviction so sessions are not evicted under memory pressure, and accept that Starter writes nothing to disk, so a restart logs everyone out.",{"question":2294,"answer":2295},"Fixed window or sliding window?","A fixed window is two commands and fine for login and signup limits, but a client can send up to twice the limit across a window boundary. A sorted-set sliding window is fairer and costs one entry per request.",{"question":2297,"answer":2298},"Does this work on Vercel or Netlify?","On serverless functions that run on the Node.js runtime, yes: open the connection at module scope and use a rediss:\u002F\u002F URL. Edge runtimes cannot open raw TCP connections, so these clients do not run there.",{"question":2300,"answer":2301},"How many sessions fit in 256 MiB?","Starter allows 192 MB of data. At about 1 KB per session that is roughly 190,000 sessions before other keys; measure your own average with MEMORY USAGE on a real session key.",null,{},"\u002Fuse-cases\u002Fsessions-and-rate-limiting","building",[],[2308,2265],"\u002Fuse-cases\u002Fai-agent-memory",{"title":5,"description":2287},[2311,2314,2317],{"label":2312,"url":2313,"checked":2286},"EXPIRE command (NX option), Valkey documentation","https:\u002F\u002Fvalkey.io\u002Fcommands\u002Fexpire\u002F",{"label":2315,"url":2316,"checked":2286},"SET command (NX, PX), Valkey documentation","https:\u002F\u002Fvalkey.io\u002Fcommands\u002Fset\u002F",{"label":2318,"url":2319,"checked":2286},"unstorage Redis driver","https:\u002F\u002Funstorage.unjs.io\u002Fdrivers\u002Fredis","use-cases\u002Fsessions-and-rate-limiting","GQo41SvnhcaBpmN9LbDvv0YMfqHRI6NFpzDnCctyUnc",[2323,2327],{"path":2308,"title":2324,"answer":2325,"description":2326,"published":327},"Memory and a semantic cache for AI agents on Valkey or Redis","One Valkey or Redis instance can hold an agent's chat history as JSON, its long-term memory as an HNSW vector index, and a semantic cache that skips the LLM call for a question already answered. On KVNest this needs the JSON, Bloom and Search modules, which you turn on per instance.","Chat history in JSON, long-term memory with HNSW vector search, a semantic cache and Bloom dedupe on one instance. Node.js code for KVNest, hosted in Germany.",{"path":2265,"title":2328,"answer":2329,"description":2330,"published":327},"EU managed Redis & Valkey: 11 providers compared (Nov 2026)","Seven of the 11 providers keep both the company and the data in the EU: Aiven, OVHcloud, Scaleway, Clever Cloud, UpCloud, Stackhero and KVNest. UpCloud is the cheapest with data on disk, at €8 a month for 1 GB. As of 4 November 2026, KVNest is the only one of the 11 that combines a free tier, a hosted MCP server and a choice of Redis or Valkey.","Who owns them, where your data lives, what the free tier and smallest plan cost, and who runs a hosted MCP server. Checked against each provider's own pages.",1793839178680]