[{"data":1,"prerenderedAt":1608},["ShallowReactive",2],{"post-\u002Fblog\u002Flist-pagination-1000-pages":3,"blog-index":1527},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"author":11,"tags":12,"guide":16,"related":17,"body":20,"_type":1521,"_id":1522,"_source":1523,"_file":1524,"_stem":1525,"_extension":1526},"\u002Fblog\u002Flist-pagination-1000-pages","blog",false,"","S3 LIST returns 1,000 entries a page, folders included","One ListObjectsV2 response holds at most 1,000 entries, and a folder rollup spends one of them. Measured on two endpoints, plus the loop that stops too early.","2026-11-11","Clement Baranger",[13,14,15],"s3","api","performance","s3-compatible-storage-desktop-client",[18,19],"s3-total-cost-of-ownership","s3-explorer-walkthrough",{"type":21,"children":22,"toc":1512},"root",[23,48,55,109,115,133,169,174,183,212,218,239,412,481,528,558,588,665,691,724,730,747,1032,1055,1072,1137,1154,1171,1188,1194,1253,1259,1381,1387,1506],{"type":24,"tag":25,"props":26,"children":27},"element","p",{},[28,31,38,40,46],{"type":29,"value":30},"text","A cleanup script lists a bucket, deletes what it finds, prints ",{"type":24,"tag":32,"props":33,"children":35},"code",{"className":34},[],[36],{"type":29,"value":37},"done",{"type":29,"value":39},", and leaves 97% of the objects in place. Nothing errored. The script asked for the bucket contents once, received a page of 1,000 entries, and treated that page as the bucket. Every S3-compatible endpoint behaves this way, and the response says so — in ",{"type":24,"tag":32,"props":41,"children":43},{"className":42},[],[44],{"type":29,"value":45},"IsTruncated",{"type":29,"value":47},", a field most code never reads.",{"type":24,"tag":49,"props":50,"children":52},"h2",{"id":51},"the-short-version",[53],{"type":29,"value":54},"The short version",{"type":24,"tag":56,"props":57,"children":58},"ul",{},[59,65,70,91,104],{"type":24,"tag":60,"props":61,"children":62},"li",{},[63],{"type":29,"value":64},"One list request returns at most 1,000 entries. A bucket holding ten million objects takes ten thousand requests to walk.",{"type":24,"tag":60,"props":66,"children":67},{},[68],{"type":29,"value":69},"A folder in the response is one of those 1,000 entries. A page can come back with 10 folders and 990 files, and still be full.",{"type":24,"tag":60,"props":71,"children":72},{},[73,75,81,83,89],{"type":29,"value":74},"The end of a listing is signalled by ",{"type":24,"tag":32,"props":76,"children":78},{"className":77},[],[79],{"type":29,"value":80},"IsTruncated: false",{"type":29,"value":82},". A page shorter than ",{"type":24,"tag":32,"props":84,"children":86},{"className":85},[],[87],{"type":29,"value":88},"MaxKeys",{"type":29,"value":90}," is not the end.",{"type":24,"tag":60,"props":92,"children":93},{},[94,96,102],{"type":29,"value":95},"Asking for ",{"type":24,"tag":32,"props":97,"children":99},{"className":98},[],[100],{"type":29,"value":101},"max-keys=5000",{"type":29,"value":103}," returns 1,000 entries on a real endpoint and 2,501 on the mock server most tests use — so a passing test proves nothing about production.",{"type":24,"tag":60,"props":105,"children":106},{},[107],{"type":29,"value":108},"Ten million objects is 10,000 billed requests to enumerate: $0.045 on Cloudflare R2 at $4.50 per million Class A operations. The script that misses objects costs more than the listing.",{"type":24,"tag":49,"props":110,"children":112},{"id":111},"what-actually-happens",[113],{"type":29,"value":114},"What actually happens",{"type":24,"tag":25,"props":116,"children":117},{},[118,124,126,131],{"type":24,"tag":32,"props":119,"children":121},{"className":120},[],[122],{"type":29,"value":123},"ListObjectsV2",{"type":29,"value":125}," answers with a page. The API reference opens with \"Returns some or all (up to 1,000) of the objects in a bucket with each request\", and the ",{"type":24,"tag":32,"props":127,"children":129},{"className":128},[],[130],{"type":29,"value":88},{"type":29,"value":132}," parameter repeats it: \"By default, the action returns up to 1,000 key names. The response might contain fewer keys but will never contain more.\"",{"type":24,"tag":25,"props":134,"children":135},{},[136,138,144,146,152,154,159,161,167],{"type":29,"value":137},"So 1,000 is not a default you can raise. It is the width of the pipe. Two fields describe what came back: ",{"type":24,"tag":32,"props":139,"children":141},{"className":140},[],[142],{"type":29,"value":143},"Contents",{"type":29,"value":145},", the objects, and ",{"type":24,"tag":32,"props":147,"children":149},{"className":148},[],[150],{"type":29,"value":151},"KeyCount",{"type":29,"value":153},", the number of entries the page actually carried. A third, ",{"type":24,"tag":32,"props":155,"children":157},{"className":156},[],[158],{"type":29,"value":45},{"type":29,"value":160},", says whether another page exists, and ",{"type":24,"tag":32,"props":162,"children":164},{"className":163},[],[165],{"type":29,"value":166},"NextContinuationToken",{"type":29,"value":168}," — \"obfuscated and is not a real key\" — carries the position forward.",{"type":24,"tag":25,"props":170,"children":171},{},[172],{"type":29,"value":173},"The ceiling costs nothing per object: AWS's own guidance says list performance \"is not substantially affected by the total number of keys in your bucket\". A million keys and a thousand keys answer in the same time. What changes is how many round trips you need, and each one is billed.",{"type":24,"tag":25,"props":175,"children":176},{},[177],{"type":24,"tag":178,"props":179,"children":182},"img",{"alt":180,"src":181},"Three ListObjectsV2 requests over 2,501 keys returning 1,000, 1,000 and 501 entries, then the same bucket listed with a delimiter where 990 objects and 10 folders together fill the 1,000-entry budget, so a loop comparing the object count with MaxKeys exits after one page","\u002Fdiagrams\u002Flist-pagination-1000-pages.svg",[],{"type":24,"tag":25,"props":184,"children":185},{},[186,188,194,196,202,204,210],{"type":29,"value":187},"Now the part that breaks code. Add ",{"type":24,"tag":32,"props":189,"children":191},{"className":190},[],[192],{"type":29,"value":193},"delimiter=\u002F",{"type":29,"value":195}," and the response rolls up everything under a common prefix into a single ",{"type":24,"tag":32,"props":197,"children":199},{"className":198},[],[200],{"type":29,"value":201},"CommonPrefixes",{"type":29,"value":203}," entry, the thing a GUI draws as a folder. The API reference is explicit that the rollup is not free: \"Each rolled-up result counts as only one return against the MaxKeys value.\" A page is therefore up to 1,000 ",{"type":24,"tag":205,"props":206,"children":207},"em",{},[208],{"type":29,"value":209},"entries",{"type":29,"value":211},", not 1,000 objects, and the mix is whatever the key order produces.",{"type":24,"tag":49,"props":213,"children":215},{"id":214},"where-it-bites",[216],{"type":29,"value":217},"Where it bites",{"type":24,"tag":25,"props":219,"children":220},{},[221,223,229,231,237],{"type":29,"value":222},"I filled a bucket with a known layout and paged through it on two endpoints: SeaweedFS 4.50 (",{"type":24,"tag":32,"props":224,"children":226},{"className":225},[],[227],{"type":29,"value":228},"weed server -s3",{"type":29,"value":230},", S3 gateway on a local port) and moto 5.2.5, the Python mock that a large share of S3 test suites run against. Both were queried with ",{"type":24,"tag":32,"props":232,"children":234},{"className":233},[],[235],{"type":29,"value":236},"boto3",{"type":29,"value":238}," 1.44.0 on 11 November 2026.",{"type":24,"tag":240,"props":241,"children":242},"table",{},[243,297],{"type":24,"tag":244,"props":245,"children":246},"thead",{},[247],{"type":24,"tag":248,"props":249,"children":250},"tr",{},[251,257,268,276,292],{"type":24,"tag":252,"props":253,"children":254},"th",{},[255],{"type":29,"value":256},"Endpoint",{"type":24,"tag":252,"props":258,"children":259},{},[260,262],{"type":29,"value":261},"2,501 keys under ",{"type":24,"tag":32,"props":263,"children":265},{"className":264},[],[266],{"type":29,"value":267},"flat\u002F",{"type":24,"tag":252,"props":269,"children":270},{},[271],{"type":24,"tag":32,"props":272,"children":274},{"className":273},[],[275],{"type":29,"value":101},{"type":24,"tag":252,"props":277,"children":278},{},[279,284,286],{"type":24,"tag":32,"props":280,"children":282},{"className":281},[],[283],{"type":29,"value":193},{"type":29,"value":285}," over ",{"type":24,"tag":32,"props":287,"children":289},{"className":288},[],[290],{"type":29,"value":291},"mixed\u002F",{"type":24,"tag":252,"props":293,"children":294},{},[295],{"type":29,"value":296},"Token returned",{"type":24,"tag":298,"props":299,"children":300},"tbody",{},[301,368],{"type":24,"tag":248,"props":302,"children":303},{},[304,310,315,326,357],{"type":24,"tag":305,"props":306,"children":307},"td",{},[308],{"type":29,"value":309},"SeaweedFS 4.50",{"type":24,"tag":305,"props":311,"children":312},{},[313],{"type":29,"value":314},"3 pages: 1,000 \u002F 1,000 \u002F 501",{"type":24,"tag":305,"props":316,"children":317},{},[318,320],{"type":29,"value":319},"1,000 entries, ",{"type":24,"tag":32,"props":321,"children":323},{"className":322},[],[324],{"type":29,"value":325},"IsTruncated: true",{"type":24,"tag":305,"props":327,"children":328},{},[329,331,336,338,343,345,350,352],{"type":29,"value":330},"990 ",{"type":24,"tag":32,"props":332,"children":334},{"className":333},[],[335],{"type":29,"value":143},{"type":29,"value":337}," + 10 ",{"type":24,"tag":32,"props":339,"children":341},{"className":340},[],[342],{"type":29,"value":201},{"type":29,"value":344},", ",{"type":24,"tag":32,"props":346,"children":348},{"className":347},[],[349],{"type":29,"value":151},{"type":29,"value":351}," 1,000, ",{"type":24,"tag":32,"props":353,"children":355},{"className":354},[],[356],{"type":29,"value":325},{"type":24,"tag":305,"props":358,"children":359},{},[360,362],{"type":29,"value":361},"the last key, ",{"type":24,"tag":32,"props":363,"children":365},{"className":364},[],[366],{"type":29,"value":367},"flat\u002F0999",{"type":24,"tag":248,"props":369,"children":370},{},[371,376,380,395,407],{"type":24,"tag":305,"props":372,"children":373},{},[374],{"type":29,"value":375},"moto 5.2.5",{"type":24,"tag":305,"props":377,"children":378},{},[379],{"type":29,"value":314},{"type":24,"tag":305,"props":381,"children":382},{},[383,389,390],{"type":24,"tag":384,"props":385,"children":386},"strong",{},[387],{"type":29,"value":388},"2,501 entries",{"type":29,"value":344},{"type":24,"tag":32,"props":391,"children":393},{"className":392},[],[394],{"type":29,"value":80},{"type":24,"tag":305,"props":396,"children":397},{},[398,400,405],{"type":29,"value":399},"identical: 990 + 10, ",{"type":24,"tag":32,"props":401,"children":403},{"className":402},[],[404],{"type":29,"value":151},{"type":29,"value":406}," 1,000",{"type":24,"tag":305,"props":408,"children":409},{},[410],{"type":29,"value":411},"32 hex characters",{"type":24,"tag":413,"props":414,"children":418},"pre",{"className":415,"code":416,"language":417,"meta":7,"style":7},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast","python3 scripts\u002Flist-pagination-probe.py fill http:\u002F\u002F127.0.0.1:8333 listprobe\npython3 scripts\u002Flist-pagination-probe.py run  http:\u002F\u002F127.0.0.1:8333 listprobe\n","bash",[419],{"type":24,"tag":32,"props":420,"children":421},{"__ignoreMap":7},[422,455],{"type":24,"tag":423,"props":424,"children":427},"span",{"class":425,"line":426},"line",1,[428,434,440,445,450],{"type":24,"tag":423,"props":429,"children":431},{"style":430},"--shiki-default:#702C00;--shiki-dark:#FFB757",[432],{"type":29,"value":433},"python3",{"type":24,"tag":423,"props":435,"children":437},{"style":436},"--shiki-default:#032563;--shiki-dark:#ADDCFF",[438],{"type":29,"value":439}," scripts\u002Flist-pagination-probe.py",{"type":24,"tag":423,"props":441,"children":442},{"style":436},[443],{"type":29,"value":444}," fill",{"type":24,"tag":423,"props":446,"children":447},{"style":436},[448],{"type":29,"value":449}," http:\u002F\u002F127.0.0.1:8333",{"type":24,"tag":423,"props":451,"children":452},{"style":436},[453],{"type":29,"value":454}," listprobe\n",{"type":24,"tag":423,"props":456,"children":458},{"class":425,"line":457},2,[459,463,467,472,477],{"type":24,"tag":423,"props":460,"children":461},{"style":430},[462],{"type":29,"value":433},{"type":24,"tag":423,"props":464,"children":465},{"style":436},[466],{"type":29,"value":439},{"type":24,"tag":423,"props":468,"children":469},{"style":436},[470],{"type":29,"value":471}," run",{"type":24,"tag":423,"props":473,"children":474},{"style":436},[475],{"type":29,"value":476},"  http:\u002F\u002F127.0.0.1:8333",{"type":24,"tag":423,"props":478,"children":479},{"style":436},[480],{"type":29,"value":454},{"type":24,"tag":25,"props":482,"children":483},{},[484,496,498,503,505,510,512,518,520,526],{"type":24,"tag":384,"props":485,"children":486},{},[487,489,494],{"type":29,"value":488},"The most expensive bug is a comparison against ",{"type":24,"tag":32,"props":490,"children":492},{"className":491},[],[493],{"type":29,"value":88},{"type":29,"value":495},".",{"type":29,"value":497}," The page over ",{"type":24,"tag":32,"props":499,"children":501},{"className":500},[],[502],{"type":29,"value":291},{"type":29,"value":504}," came back with 990 objects and 10 folders. It is completely full — ",{"type":24,"tag":32,"props":506,"children":508},{"className":507},[],[509],{"type":29,"value":151},{"type":29,"value":511}," is 1,000 — but ",{"type":24,"tag":32,"props":513,"children":515},{"className":514},[],[516],{"type":29,"value":517},"len(Contents)",{"type":29,"value":519}," is 990, so ",{"type":24,"tag":32,"props":521,"children":523},{"className":522},[],[524],{"type":29,"value":525},"if len(Contents) \u003C MaxKeys: break",{"type":29,"value":527}," exits after one request. In this layout the 40 folders that were never listed hold another 2,000 keys. The script reports success because no call failed. Both endpoints produced this page identically; it is not a quirk, it is the contract.",{"type":24,"tag":25,"props":529,"children":530},{},[531,542,544,549,551,556],{"type":24,"tag":384,"props":532,"children":533},{},[534,540],{"type":24,"tag":32,"props":535,"children":537},{"className":536},[],[538],{"type":29,"value":539},"max-keys",{"type":29,"value":541}," above 1,000 is a request, not a setting.",{"type":29,"value":543}," SeaweedFS clamped it to 1,000 and set ",{"type":24,"tag":32,"props":545,"children":547},{"className":546},[],[548],{"type":29,"value":325},{"type":29,"value":550},". moto returned all 2,501 entries in one response and set ",{"type":24,"tag":32,"props":552,"children":554},{"className":553},[],[555],{"type":29,"value":80},{"type":29,"value":557}," — behaviour AWS's documentation rules out. A test suite on the mock sees a complete listing from a single call, then production hands the same code 1,000 entries and a token it ignores.",{"type":24,"tag":25,"props":559,"children":560},{},[561,579,581,586],{"type":24,"tag":384,"props":562,"children":563},{},[564,570,572,577],{"type":24,"tag":32,"props":565,"children":567},{"className":566},[],[568],{"type":29,"value":569},"max-keys=0",{"type":29,"value":571}," returns an empty page with ",{"type":24,"tag":32,"props":573,"children":575},{"className":574},[],[576],{"type":29,"value":80},{"type":29,"value":578}," on both endpoints",{"type":29,"value":580}," — the probe for \"how many objects are here\" reads ",{"type":24,"tag":32,"props":582,"children":584},{"className":583},[],[585],{"type":29,"value":151},{"type":29,"value":587},", gets zero, and reports an empty bucket with no error.",{"type":24,"tag":25,"props":589,"children":590},{},[591,596,598,604,605,611,612,618,619,625,626,632,634,640,642,648,650,656,657,663],{"type":24,"tag":384,"props":592,"children":593},{},[594],{"type":29,"value":595},"Order is UTF-8 binary, not sensible.",{"type":29,"value":597}," The five keys ",{"type":24,"tag":32,"props":599,"children":601},{"className":600},[],[602],{"type":29,"value":603},"order\u002F1",{"type":29,"value":344},{"type":24,"tag":32,"props":606,"children":608},{"className":607},[],[609],{"type":29,"value":610},"order\u002F2",{"type":29,"value":344},{"type":24,"tag":32,"props":613,"children":615},{"className":614},[],[616],{"type":29,"value":617},"order\u002F9",{"type":29,"value":344},{"type":24,"tag":32,"props":620,"children":622},{"className":621},[],[623],{"type":29,"value":624},"order\u002F10",{"type":29,"value":344},{"type":24,"tag":32,"props":627,"children":629},{"className":628},[],[630],{"type":29,"value":631},"order\u002F100",{"type":29,"value":633}," came back as ",{"type":24,"tag":32,"props":635,"children":637},{"className":636},[],[638],{"type":29,"value":639},"1, 10, 100, 2, 9",{"type":29,"value":641},". Date-suffixed keys sort wrong at the month boundary (",{"type":24,"tag":32,"props":643,"children":645},{"className":644},[],[646],{"type":29,"value":647},"2026-11-1",{"type":29,"value":649}," before ",{"type":24,"tag":32,"props":651,"children":653},{"className":652},[],[654],{"type":29,"value":655},"2026-11-10",{"type":29,"value":649},{"type":24,"tag":32,"props":658,"children":660},{"className":659},[],[661],{"type":29,"value":662},"2026-11-2",{"type":29,"value":664},"), and numeric IDs sort as text. Anything that pages by \"last key seen\" inherits that order, so a resumable job restarts in the wrong place when the naming scheme is numeric.",{"type":24,"tag":25,"props":666,"children":667},{},[668,673,675,681,683,689],{"type":24,"tag":384,"props":669,"children":670},{},[671],{"type":29,"value":672},"The token looks like a key on one endpoint and a hash on the other.",{"type":29,"value":674}," SeaweedFS answered ",{"type":24,"tag":32,"props":676,"children":678},{"className":677},[],[679],{"type":29,"value":680},"NextContinuationToken: flat\u002F0999",{"type":29,"value":682}," — the last key of the page. moto answered a 32-character hex string. Code that treats the token as ",{"type":24,"tag":32,"props":684,"children":686},{"className":685},[],[687],{"type":29,"value":688},"StartAfter",{"type":29,"value":690},", or persists it across an upgrade, works on the self-hosted server and fails on AWS. AWS documents the token as obfuscated; its shape is not part of the contract.",{"type":24,"tag":25,"props":692,"children":693},{},[694,699,701,706,708,715,717,723],{"type":24,"tag":384,"props":695,"children":696},{},[697],{"type":29,"value":698},"Browsing is not free either.",{"type":29,"value":700}," AWS bills LIST requests in the same class as PUT, and the pricing page states that browsing the console \"incur",{"type":24,"tag":423,"props":702,"children":703},{},[704],{"type":29,"value":705},"s",{"type":29,"value":707}," charges for GET, LIST, and other requests that are made to facilitate browsing\". A person clicking through folders generates exactly the calls a script does — ",{"type":24,"tag":709,"props":710,"children":712},"a",{"href":711},"\u002Fblog\u002Fs3-explorer-walkthrough",[713],{"type":29,"value":714},"the S3 Explorer walkthrough",{"type":29,"value":716}," shows that browse call by call — and the invoice consequence is set out in ",{"type":24,"tag":709,"props":718,"children":720},{"href":719},"\u002Fblog\u002Fs3-total-cost-of-ownership",[721],{"type":29,"value":722},"the request charges that make up most of an S3 bill",{"type":29,"value":495},{"type":24,"tag":49,"props":725,"children":727},{"id":726},"doing-it-right",[728],{"type":29,"value":729},"Doing it right",{"type":24,"tag":25,"props":731,"children":732},{},[733,745],{"type":24,"tag":384,"props":734,"children":735},{},[736,738,743],{"type":29,"value":737},"1. Loop on ",{"type":24,"tag":32,"props":739,"children":741},{"className":740},[],[742],{"type":29,"value":45},{"type":29,"value":744}," and nothing else.",{"type":29,"value":746}," The SDK paginator does it for you; hand-rolled loops should copy its shape exactly.",{"type":24,"tag":413,"props":748,"children":752},{"className":749,"code":750,"language":751,"meta":7,"style":7},"language-python shiki shiki-themes github-light-high-contrast github-dark-high-contrast","paginator = s3.get_paginator(\"list_objects_v2\")\ntotal = 0\nfor page in paginator.paginate(Bucket=\"backups-prod\", Prefix=\"db\u002F\", PaginationConfig={\"PageSize\": 1000}):\n    total += page[\"KeyCount\"]            # objects AND rolled-up prefixes\n    for obj in page.get(\"Contents\", []):\n        process(obj[\"Key\"])\nprint(f\"{total} entries listed\")\n","python",[753],{"type":24,"tag":32,"props":754,"children":755},{"__ignoreMap":7},[756,786,804,898,933,966,985],{"type":24,"tag":423,"props":757,"children":758},{"class":425,"line":426},[759,765,771,776,781],{"type":24,"tag":423,"props":760,"children":762},{"style":761},"--shiki-default:#0E1116;--shiki-dark:#F0F3F6",[763],{"type":29,"value":764},"paginator ",{"type":24,"tag":423,"props":766,"children":768},{"style":767},"--shiki-default:#A0111F;--shiki-dark:#FF9492",[769],{"type":29,"value":770},"=",{"type":24,"tag":423,"props":772,"children":773},{"style":761},[774],{"type":29,"value":775}," s3.get_paginator(",{"type":24,"tag":423,"props":777,"children":778},{"style":436},[779],{"type":29,"value":780},"\"list_objects_v2\"",{"type":24,"tag":423,"props":782,"children":783},{"style":761},[784],{"type":29,"value":785},")\n",{"type":24,"tag":423,"props":787,"children":788},{"class":425,"line":457},[789,794,798],{"type":24,"tag":423,"props":790,"children":791},{"style":761},[792],{"type":29,"value":793},"total ",{"type":24,"tag":423,"props":795,"children":796},{"style":767},[797],{"type":29,"value":770},{"type":24,"tag":423,"props":799,"children":801},{"style":800},"--shiki-default:#023B95;--shiki-dark:#91CBFF",[802],{"type":29,"value":803}," 0\n",{"type":24,"tag":423,"props":805,"children":807},{"class":425,"line":806},3,[808,813,818,823,828,833,837,842,846,851,855,860,864,869,873,878,883,888,893],{"type":24,"tag":423,"props":809,"children":810},{"style":767},[811],{"type":29,"value":812},"for",{"type":24,"tag":423,"props":814,"children":815},{"style":761},[816],{"type":29,"value":817}," page ",{"type":24,"tag":423,"props":819,"children":820},{"style":767},[821],{"type":29,"value":822},"in",{"type":24,"tag":423,"props":824,"children":825},{"style":761},[826],{"type":29,"value":827}," paginator.paginate(",{"type":24,"tag":423,"props":829,"children":830},{"style":430},[831],{"type":29,"value":832},"Bucket",{"type":24,"tag":423,"props":834,"children":835},{"style":767},[836],{"type":29,"value":770},{"type":24,"tag":423,"props":838,"children":839},{"style":436},[840],{"type":29,"value":841},"\"backups-prod\"",{"type":24,"tag":423,"props":843,"children":844},{"style":761},[845],{"type":29,"value":344},{"type":24,"tag":423,"props":847,"children":848},{"style":430},[849],{"type":29,"value":850},"Prefix",{"type":24,"tag":423,"props":852,"children":853},{"style":767},[854],{"type":29,"value":770},{"type":24,"tag":423,"props":856,"children":857},{"style":436},[858],{"type":29,"value":859},"\"db\u002F\"",{"type":24,"tag":423,"props":861,"children":862},{"style":761},[863],{"type":29,"value":344},{"type":24,"tag":423,"props":865,"children":866},{"style":430},[867],{"type":29,"value":868},"PaginationConfig",{"type":24,"tag":423,"props":870,"children":871},{"style":767},[872],{"type":29,"value":770},{"type":24,"tag":423,"props":874,"children":875},{"style":761},[876],{"type":29,"value":877},"{",{"type":24,"tag":423,"props":879,"children":880},{"style":436},[881],{"type":29,"value":882},"\"PageSize\"",{"type":24,"tag":423,"props":884,"children":885},{"style":761},[886],{"type":29,"value":887},": ",{"type":24,"tag":423,"props":889,"children":890},{"style":800},[891],{"type":29,"value":892},"1000",{"type":24,"tag":423,"props":894,"children":895},{"style":761},[896],{"type":29,"value":897},"}):\n",{"type":24,"tag":423,"props":899,"children":901},{"class":425,"line":900},4,[902,907,912,917,922,927],{"type":24,"tag":423,"props":903,"children":904},{"style":761},[905],{"type":29,"value":906},"    total ",{"type":24,"tag":423,"props":908,"children":909},{"style":767},[910],{"type":29,"value":911},"+=",{"type":24,"tag":423,"props":913,"children":914},{"style":761},[915],{"type":29,"value":916}," page[",{"type":24,"tag":423,"props":918,"children":919},{"style":436},[920],{"type":29,"value":921},"\"KeyCount\"",{"type":24,"tag":423,"props":923,"children":924},{"style":761},[925],{"type":29,"value":926},"]            ",{"type":24,"tag":423,"props":928,"children":930},{"style":929},"--shiki-default:#66707B;--shiki-dark:#BDC4CC",[931],{"type":29,"value":932},"# objects AND rolled-up prefixes\n",{"type":24,"tag":423,"props":934,"children":936},{"class":425,"line":935},5,[937,942,947,951,956,961],{"type":24,"tag":423,"props":938,"children":939},{"style":767},[940],{"type":29,"value":941},"    for",{"type":24,"tag":423,"props":943,"children":944},{"style":761},[945],{"type":29,"value":946}," obj ",{"type":24,"tag":423,"props":948,"children":949},{"style":767},[950],{"type":29,"value":822},{"type":24,"tag":423,"props":952,"children":953},{"style":761},[954],{"type":29,"value":955}," page.get(",{"type":24,"tag":423,"props":957,"children":958},{"style":436},[959],{"type":29,"value":960},"\"Contents\"",{"type":24,"tag":423,"props":962,"children":963},{"style":761},[964],{"type":29,"value":965},", []):\n",{"type":24,"tag":423,"props":967,"children":969},{"class":425,"line":968},6,[970,975,980],{"type":24,"tag":423,"props":971,"children":972},{"style":761},[973],{"type":29,"value":974},"        process(obj[",{"type":24,"tag":423,"props":976,"children":977},{"style":436},[978],{"type":29,"value":979},"\"Key\"",{"type":24,"tag":423,"props":981,"children":982},{"style":761},[983],{"type":29,"value":984},"])\n",{"type":24,"tag":423,"props":986,"children":988},{"class":425,"line":987},7,[989,994,999,1004,1009,1013,1018,1023,1028],{"type":24,"tag":423,"props":990,"children":991},{"style":800},[992],{"type":29,"value":993},"print",{"type":24,"tag":423,"props":995,"children":996},{"style":761},[997],{"type":29,"value":998},"(",{"type":24,"tag":423,"props":1000,"children":1001},{"style":767},[1002],{"type":29,"value":1003},"f",{"type":24,"tag":423,"props":1005,"children":1006},{"style":436},[1007],{"type":29,"value":1008},"\"",{"type":24,"tag":423,"props":1010,"children":1011},{"style":767},[1012],{"type":29,"value":877},{"type":24,"tag":423,"props":1014,"children":1015},{"style":761},[1016],{"type":29,"value":1017},"total",{"type":24,"tag":423,"props":1019,"children":1020},{"style":767},[1021],{"type":29,"value":1022},"}",{"type":24,"tag":423,"props":1024,"children":1025},{"style":436},[1026],{"type":29,"value":1027}," entries listed\"",{"type":24,"tag":423,"props":1029,"children":1030},{"style":761},[1031],{"type":29,"value":785},{"type":24,"tag":25,"props":1033,"children":1034},{},[1035,1053],{"type":24,"tag":384,"props":1036,"children":1037},{},[1038,1040,1045,1047,1052],{"type":29,"value":1039},"2. Count entries with ",{"type":24,"tag":32,"props":1041,"children":1043},{"className":1042},[],[1044],{"type":29,"value":151},{"type":29,"value":1046},", never with ",{"type":24,"tag":32,"props":1048,"children":1050},{"className":1049},[],[1051],{"type":29,"value":517},{"type":29,"value":495},{"type":29,"value":1054}," The difference between the two is exactly the number of folders on that page.",{"type":24,"tag":25,"props":1056,"children":1057},{},[1058,1070],{"type":24,"tag":384,"props":1059,"children":1060},{},[1061,1063,1068],{"type":29,"value":1062},"3. Use ",{"type":24,"tag":32,"props":1064,"children":1066},{"className":1065},[],[1067],{"type":29,"value":688},{"type":29,"value":1069}," when the job must be restartable.",{"type":29,"value":1071}," AWS documents it as \"where you want Amazon S3 to start listing from\" and accepts \"any key in the bucket\", so a crashed job can resume from a key you persisted, without storing an opaque token that may not survive a client upgrade.",{"type":24,"tag":413,"props":1073,"children":1075},{"className":415,"code":1074,"language":417,"meta":7,"style":7},"aws s3api list-objects-v2 --bucket backups-prod --prefix db\u002F --max-keys 1000 --start-after db\u002F2026-11-09.tar\n",[1076],{"type":24,"tag":32,"props":1077,"children":1078},{"__ignoreMap":7},[1079],{"type":24,"tag":423,"props":1080,"children":1081},{"class":425,"line":426},[1082,1087,1092,1097,1102,1107,1112,1117,1122,1127,1132],{"type":24,"tag":423,"props":1083,"children":1084},{"style":430},[1085],{"type":29,"value":1086},"aws",{"type":24,"tag":423,"props":1088,"children":1089},{"style":436},[1090],{"type":29,"value":1091}," s3api",{"type":24,"tag":423,"props":1093,"children":1094},{"style":436},[1095],{"type":29,"value":1096}," list-objects-v2",{"type":24,"tag":423,"props":1098,"children":1099},{"style":800},[1100],{"type":29,"value":1101}," --bucket",{"type":24,"tag":423,"props":1103,"children":1104},{"style":436},[1105],{"type":29,"value":1106}," backups-prod",{"type":24,"tag":423,"props":1108,"children":1109},{"style":800},[1110],{"type":29,"value":1111}," --prefix",{"type":24,"tag":423,"props":1113,"children":1114},{"style":436},[1115],{"type":29,"value":1116}," db\u002F",{"type":24,"tag":423,"props":1118,"children":1119},{"style":800},[1120],{"type":29,"value":1121}," --max-keys",{"type":24,"tag":423,"props":1123,"children":1124},{"style":800},[1125],{"type":29,"value":1126}," 1000",{"type":24,"tag":423,"props":1128,"children":1129},{"style":800},[1130],{"type":29,"value":1131}," --start-after",{"type":24,"tag":423,"props":1133,"children":1134},{"style":436},[1135],{"type":29,"value":1136}," db\u002F2026-11-09.tar\n",{"type":24,"tag":25,"props":1138,"children":1139},{},[1140,1152],{"type":24,"tag":384,"props":1141,"children":1142},{},[1143,1145,1150],{"type":29,"value":1144},"4. Set ",{"type":24,"tag":32,"props":1146,"children":1148},{"className":1147},[],[1149],{"type":29,"value":88},{"type":29,"value":1151}," below the ceiling when you need predictable pages.",{"type":29,"value":1153}," A page size of 500 makes retries cheaper and keeps a single slow response from stalling the whole walk. Above 1,000 is silently clamped on real endpoints.",{"type":24,"tag":25,"props":1155,"children":1156},{},[1157,1162,1164,1169],{"type":24,"tag":384,"props":1158,"children":1159},{},[1160],{"type":29,"value":1161},"5. Prove the cap on the endpoint you deploy against, not on the mock.",{"type":29,"value":1163}," One run of the probe above answers what no test double can: whether this provider honours ",{"type":24,"tag":32,"props":1165,"children":1167},{"className":1166},[],[1168],{"type":29,"value":88},{"type":29,"value":1170},", whether prefixes share the budget, and what its token looks like.",{"type":24,"tag":25,"props":1172,"children":1173},{},[1174,1179,1181,1186],{"type":24,"tag":384,"props":1175,"children":1176},{},[1177],{"type":29,"value":1178},"6. Verify a deletion or backup job against an independent count.",{"type":29,"value":1180}," Log the entries processed and compare with the number the provider reports elsewhere: a job that lists 1,000 entries from a 30,000-object bucket and logs ",{"type":24,"tag":32,"props":1182,"children":1184},{"className":1183},[],[1185],{"type":29,"value":37},{"type":29,"value":1187}," is indistinguishable from success in every log you have.",{"type":24,"tag":49,"props":1189,"children":1191},{"id":1190},"how-s3-explorer-fits",[1192],{"type":29,"value":1193},"How S3 Explorer fits",{"type":24,"tag":25,"props":1195,"children":1196},{},[1197,1199,1205,1207,1212,1214,1220,1221,1227,1229,1234,1236,1241,1243,1251],{"type":29,"value":1198},"Folder navigation in a desktop client is the same ",{"type":24,"tag":32,"props":1200,"children":1202},{"className":1201},[],[1203],{"type":29,"value":1204},"delimiter",{"type":29,"value":1206}," mechanism — each click is another LIST, and a folder holding more than 1,000 entries needs more than one. S3 Explorer's ",{"type":24,"tag":384,"props":1208,"children":1209},{},[1210],{"type":29,"value":1211},"per-request metrics",{"type":29,"value":1213}," dashboard counts the API calls a session generated (",{"type":24,"tag":32,"props":1215,"children":1217},{"className":1216},[],[1218],{"type":29,"value":1219},"LIST",{"type":29,"value":344},{"type":24,"tag":32,"props":1222,"children":1224},{"className":1223},[],[1225],{"type":29,"value":1226},"GET",{"type":29,"value":1228}," and the rest), computed locally, so a browse that looks like one click can be read as the number of requests it really was. The ",{"type":24,"tag":384,"props":1230,"children":1231},{},[1232],{"type":29,"value":1233},"test connection",{"type":29,"value":1235}," step reports how many buckets the credentials can see before a profile is saved, and ",{"type":24,"tag":384,"props":1237,"children":1238},{},[1239],{"type":29,"value":1240},"multiple connection profiles",{"type":29,"value":1242}," let the same folder be opened on AWS and on a self-hosted endpoint from one window. ",{"type":24,"tag":709,"props":1244,"children":1248},{"href":1245,"rel":1246},"https:\u002F\u002Fs3explorer.accessdevops.com",[1247],"nofollow",[1249],{"type":29,"value":1250},"S3 Explorer",{"type":29,"value":1252}," runs on Windows, macOS and Linux.",{"type":24,"tag":49,"props":1254,"children":1256},{"id":1255},"checklist",[1257],{"type":29,"value":1258},"Checklist",{"type":24,"tag":56,"props":1260,"children":1263},{"className":1261},[1262],"contains-task-list",[1264,1284,1307,1323,1338,1347,1363,1372],{"type":24,"tag":60,"props":1265,"children":1268},{"className":1266},[1267],"task-list-item",[1269,1275,1277,1282],{"type":24,"tag":1270,"props":1271,"children":1274},"input",{"disabled":1272,"type":1273},true,"checkbox",[],{"type":29,"value":1276}," Every list call loops on ",{"type":24,"tag":32,"props":1278,"children":1280},{"className":1279},[],[1281],{"type":29,"value":45},{"type":29,"value":1283},", or uses the SDK paginator.",{"type":24,"tag":60,"props":1285,"children":1287},{"className":1286},[1267],[1288,1291,1293,1298,1300,1305],{"type":24,"tag":1270,"props":1289,"children":1290},{"disabled":1272,"type":1273},[],{"type":29,"value":1292}," No code ends a loop by comparing ",{"type":24,"tag":32,"props":1294,"children":1296},{"className":1295},[],[1297],{"type":29,"value":517},{"type":29,"value":1299}," with ",{"type":24,"tag":32,"props":1301,"children":1303},{"className":1302},[],[1304],{"type":29,"value":88},{"type":29,"value":1306},", or with 1,000.",{"type":24,"tag":60,"props":1308,"children":1310},{"className":1309},[1267],[1311,1314,1316,1321],{"type":24,"tag":1270,"props":1312,"children":1313},{"disabled":1272,"type":1273},[],{"type":29,"value":1315}," Entry counts use ",{"type":24,"tag":32,"props":1317,"children":1319},{"className":1318},[],[1320],{"type":29,"value":151},{"type":29,"value":1322},", folders included.",{"type":24,"tag":60,"props":1324,"children":1326},{"className":1325},[1267],[1327,1330,1332,1337],{"type":24,"tag":1270,"props":1328,"children":1329},{"disabled":1272,"type":1273},[],{"type":29,"value":1331}," Nothing parses or reconstructs ",{"type":24,"tag":32,"props":1333,"children":1335},{"className":1334},[],[1336],{"type":29,"value":166},{"type":29,"value":495},{"type":24,"tag":60,"props":1339,"children":1341},{"className":1340},[1267],[1342,1345],{"type":24,"tag":1270,"props":1343,"children":1344},{"disabled":1272,"type":1273},[],{"type":29,"value":1346}," Ordering assumptions are written down: UTF-8 binary, so numeric suffixes sort as text.",{"type":24,"tag":60,"props":1348,"children":1350},{"className":1349},[1267],[1351,1354,1356,1361],{"type":24,"tag":1270,"props":1352,"children":1353},{"disabled":1272,"type":1273},[],{"type":29,"value":1355}," ",{"type":24,"tag":32,"props":1357,"children":1359},{"className":1358},[],[1360],{"type":29,"value":88},{"type":29,"value":1362}," is set explicitly and never above 1,000.",{"type":24,"tag":60,"props":1364,"children":1366},{"className":1365},[1267],[1367,1370],{"type":24,"tag":1270,"props":1368,"children":1369},{"disabled":1272,"type":1273},[],{"type":29,"value":1371}," At least one test runs against a real endpoint; the mock-only test is labelled as such.",{"type":24,"tag":60,"props":1373,"children":1375},{"className":1374},[1267],[1376,1379],{"type":24,"tag":1270,"props":1377,"children":1378},{"disabled":1272,"type":1273},[],{"type":29,"value":1380}," Cleanup and backup jobs log the entries they processed and compare against an independent count.",{"type":24,"tag":49,"props":1382,"children":1384},{"id":1383},"sources",[1385],{"type":29,"value":1386},"Sources",{"type":24,"tag":56,"props":1388,"children":1389},{},[1390,1430,1442,1460,1480],{"type":24,"tag":60,"props":1391,"children":1392},{},[1393,1400,1402,1407,1409,1414,1416,1421,1423,1428],{"type":24,"tag":709,"props":1394,"children":1397},{"href":1395,"rel":1396},"https:\u002F\u002Fdocs.aws.amazon.com\u002FAmazonS3\u002Flatest\u002FAPI\u002FAPI_ListObjectsV2.html",[1247],[1398],{"type":29,"value":1399},"Amazon S3 — ListObjectsV2",{"type":29,"value":1401},": \"up to 1,000\" per request; ",{"type":24,"tag":32,"props":1403,"children":1405},{"className":1404},[],[1406],{"type":29,"value":88},{"type":29,"value":1408}," \"will never contain more\"; ",{"type":24,"tag":32,"props":1410,"children":1412},{"className":1411},[],[1413],{"type":29,"value":151},{"type":29,"value":1415}," \"less than or equal to the MaxKeys field\"; \"Each rolled-up result counts as only one return against the MaxKeys value\"; ",{"type":24,"tag":32,"props":1417,"children":1419},{"className":1418},[],[1420],{"type":29,"value":166},{"type":29,"value":1422}," \"obfuscated and is not a real key\"; ",{"type":24,"tag":32,"props":1424,"children":1426},{"className":1425},[],[1427],{"type":29,"value":688},{"type":29,"value":1429}," starts listing after the given key.",{"type":24,"tag":60,"props":1431,"children":1432},{},[1433,1440],{"type":24,"tag":709,"props":1434,"children":1437},{"href":1435,"rel":1436},"https:\u002F\u002Fdocs.aws.amazon.com\u002FAmazonS3\u002Flatest\u002Fuserguide\u002FListingKeysUsingAPIs.html",[1247],[1438],{"type":29,"value":1439},"Amazon S3 — Listing object keys programmatically",{"type":29,"value":1441},": a page of up to 1,000 keys with a truncation indicator; results \"always returned in UTF-8 binary order\"; list performance \"not substantially affected by the total number of keys\".",{"type":24,"tag":60,"props":1443,"children":1444},{},[1445,1452,1454,1458],{"type":24,"tag":709,"props":1446,"children":1449},{"href":1447,"rel":1448},"https:\u002F\u002Faws.amazon.com\u002Fs3\u002Fpricing\u002F",[1247],[1450],{"type":29,"value":1451},"Amazon S3 — pricing",{"type":29,"value":1453},": LIST requests are billed with PUT, COPY and POST; console browsing \"incur",{"type":24,"tag":423,"props":1455,"children":1456},{},[1457],{"type":29,"value":705},{"type":29,"value":1459}," charges for GET, LIST, and other requests that are made to facilitate browsing\".",{"type":24,"tag":60,"props":1461,"children":1462},{},[1463,1470,1472,1478],{"type":24,"tag":709,"props":1464,"children":1467},{"href":1465,"rel":1466},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fr2\u002Fpricing\u002F",[1247],[1468],{"type":29,"value":1469},"Cloudflare R2 — pricing",{"type":29,"value":1471},", page last updated 30 October 2026: Class A operations $4.50 per million, and ",{"type":24,"tag":32,"props":1473,"children":1475},{"className":1474},[],[1476],{"type":29,"value":1477},"ListObjects",{"type":29,"value":1479}," is listed as a Class A operation.",{"type":24,"tag":60,"props":1481,"children":1482},{},[1483,1485,1490,1492,1498,1500,1505],{"type":29,"value":1484},"Measured 11 November 2026 against SeaweedFS 4.50 and moto 5.2.5 with ",{"type":24,"tag":32,"props":1486,"children":1488},{"className":1487},[],[1489],{"type":29,"value":236},{"type":29,"value":1491}," 1.44.0; the probe is ",{"type":24,"tag":32,"props":1493,"children":1495},{"className":1494},[],[1496],{"type":29,"value":1497},"scripts\u002Flist-pagination-probe.py",{"type":29,"value":1499}," in this repository, and the bucket layout it builds is 3,547 objects, 2,501 of them under ",{"type":24,"tag":32,"props":1501,"children":1503},{"className":1502},[],[1504],{"type":29,"value":267},{"type":29,"value":495},{"type":24,"tag":1507,"props":1508,"children":1509},"style",{},[1510],{"type":29,"value":1511},"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":7,"searchDepth":457,"depth":457,"links":1513},[1514,1515,1516,1517,1518,1519,1520],{"id":51,"depth":457,"text":54},{"id":111,"depth":457,"text":114},{"id":214,"depth":457,"text":217},{"id":726,"depth":457,"text":729},{"id":1190,"depth":457,"text":1193},{"id":1255,"depth":457,"text":1258},{"id":1383,"depth":457,"text":1386},"markdown","content:blog:list-pagination-1000-pages.md","content","blog\u002Flist-pagination-1000-pages.md","blog\u002Flist-pagination-1000-pages","md",[1528,1536,1542,1548,1554,1556,1563,1568,1574,1579,1584,1591,1598,1603],{"_path":1529,"title":1530,"description":1531,"tags":1532},"\u002Fblog\u002Fabac-object-tags-vs-prefix-policies","Object tags or prefixes: which access model survives growth","Prefixes decide what a principal can list, tags decide what it can read. Two AWS limits break tag-only designs, and prefix policies run out at about 60 tenants.",[13,1533,1534,1535],"iam","governance","access-control",{"_path":1537,"title":1538,"description":1539,"tags":1540},"\u002Fblog\u002Fassumerole-chaining-one-hour-ceiling","Chained AssumeRole sessions stop at one hour, silently","A role that assumes another role gets 3600 seconds, whatever its MaxSessionDuration allows. The exact error, and the batch job that dies at minute 61.",[13,1533,1541,1086],"sts",{"_path":1543,"title":1544,"description":1545,"tags":1546},"\u002Fblog\u002Fbucket-policy-allow-still-denied","When two policies allow and the request still fails","S3 returns AccessDenied even when the bucket and IAM policies both allow. Here is the five-gate evaluation order and where the hidden deny sits.",[13,1533,1547,1535],"security",{"_path":1549,"title":1550,"description":1551,"tags":1552},"\u002Fblog\u002Fcrc64nvme-new-integrity-default","CRC64NVME is now the S3 default, your SDK sends CRC32","S3 stores a CRC64NVME checksum on every new object, while the CLI and the SDKs default to different algorithms. Measured on a live endpoint.",[13,1553,14],"integrity",{"_path":4,"title":8,"description":9,"tags":1555},[13,14,15],{"_path":1557,"title":1558,"description":1559,"tags":1560},"\u002Fblog\u002Fmultipart-5mb-floor-10000-parts","Multipart upload: the 5 MiB floor and the 10,000-part ceiling","A single PUT stops at 5 GiB. Splitting a 60 GB file moves the failure rather than removing it: the 5 MiB part floor is checked last, after every byte is sent.",[13,1561,1562],"uploads","multipart",{"_path":1564,"title":1565,"description":1566,"tags":1567},"\u002Fblog\u002Fpresigned-post-enforce-size-content-type","Presigned POST: cap size and type before the upload starts","A presigned PUT signs a URL, not a byte count. A POST policy adds a size range and a content-type condition that S3 enforces while the body arrives.",[13,1547,1561],{"_path":1569,"title":1570,"description":1571,"tags":1572},"\u002Fblog\u002Fpresigned-url-leakage","Presigned URLs leak into logs, referrers and chat","A presigned URL is a working credential stored as text. Where the copy ends up, what it still grants after redaction, and the 15-minute fix.",[13,1547,1573],"presigned-urls",{"_path":1575,"title":1576,"description":1577,"tags":1578},"\u002Fblog\u002Fpresigned-url-seven-day-ceiling","Presigned URLs expire at seven days. Then what?","Seven days is not a setting: it is how long the SigV4 signing key lives. What happens at that wall, and the three patterns that outlive it.",[13,1547,1573],{"_path":1580,"title":1581,"description":1582,"tags":1583},"\u002Fblog\u002Fresume-interrupted-multipart-upload","Resuming a multipart upload you never meant to abandon","An upload ID never expires, so a crashed upload is resumable without the state file — measured: the part list is the real state, and it can lie about the ETag.",[13,1561,1562],{"_path":711,"title":1585,"description":1586,"tags":1587},"S3 Explorer walkthrough: zero to browsing in 5 minutes","A step-by-step walkthrough of S3 Explorer against a live S3 endpoint: pick a provider, test, browse buckets, inspect metadata, read request metrics.",[13,1588,1589,1590],"tutorial","tooling","seaweedfs",{"_path":1592,"title":1593,"description":1594,"tags":1595},"\u002Fblog\u002Fs3-in-2027-egress-bill-changes","S3 in 2027: the egress bill changes on 12 January","The EU Data Act removes switching fees in January 2027 — but not everyday egress. The five things you need to know about your buckets before that date.",[13,1596,1534,1597],"pricing","cloud",{"_path":719,"title":1599,"description":1600,"tags":1601},"The real cost of S3: 8 providers compared, egress included","Storage is 20% of your bill. We modelled 8 S3 providers across 4 workloads — backup, app, media, DR — with storage, egress, requests and replication.",[13,1596,1597,1602],"cost-optimisation",{"_path":1604,"title":1605,"description":1606,"tags":1607},"\u002Fblog\u002Fwelcome","Why we write about S3, and how this blog is made","Object storage is everywhere and most advice about it is stale. What this blog covers, how each article is sourced, and what we measure to know it helped.",[13,1597],1794568659400]