S3 ListObjectsV2: List More Than 1,000 Objects in Python, JavaScript, CLI and PowerShell, Prefixes and Folders, s3:ListBucket, and S3 Limits
S3 ListObjectsV2 returns at most 1,000 objects per call, so to list more you must paginate: when the response says IsTruncated: true, call again with its NextContinuationToken as the ContinuationToken, until it says false. You rarely write that loop yourself: in Python use s3.get_paginator("list_objects_v2"), in JavaScript paginateListObjectsV2, in the AWS CLI aws s3api list-objects-v2 (it pages for you), and in PowerShell Get-S3Object. Use Prefix to list one "folder" and Delimiter="/" to get the folders themselves in CommonPrefixes. The permission you need is s3:ListBucket on the bucket ARN (not bucket/*); there is no IAM action called ListObjects. And the S3 limits people search alongside this: objects up to 50 TB (5 GB in a single PUT, larger with multipart upload), no limit on the number of objects per bucket, and keys up to 1,024 bytes. Below: copy-ready code for each language, prefixes and delimiters, counting objects and sizes without listing millions, speed and cost, and the errors.
Jake's computer shop uploads photos of every repair to S3: before, after, and anything odd in between. Ethan's first script to count them printed exactly 1,000. So did the second, after another busy week. The bucket actually held 23,481 photos. S3 had not lost anything; the script had read only the first page of results. It is the most common S3 surprise there is, and the fix is a few lines in any language.
New to S3? Our plain-English S3 explainer covers buckets, objects and keys, and the cards below cover the rest.
Why you only get 1,000 objects, and how paging works
A bucket can hold billions of objects, so S3 never returns them all at once. Each ListObjectsV2 call returns one page of up to 1,000 keys, in order, plus enough information for you to ask for the next page:
| Field | Where | What it means |
|---|---|---|
MaxKeys | Request | How many keys you want per page; 1,000 is both the default and the maximum |
Contents | Response | The objects on this page: key, size, last modified, ETag, storage class |
IsTruncated | Response | true means more pages follow |
NextContinuationToken | Response | Pass this back as ContinuationToken to get the next page |
KeyCount | Response | How many keys (and common prefixes) are on this page |
Prefix, Delimiter, StartAfter | Request | Narrow the listing to a "folder," group by folders, or start after a given key |
Results come back in alphabetical order of key (strictly, UTF-8 binary order) in ordinary buckets, which is why paging works so neatly: each page continues where the last left off. (S3 Express One Zone "directory buckets" are the exception: their listings are not in sorted order.)
The bug in Ethan's first script is the bug in most scripts: one call, read Contents, stop. Everything below is about not stopping until IsTruncated is false, preferably by letting the SDK do it.
Jake: "So it was like reading only the first page of the phone book?"
Ethan: "Exactly, and the first page always has exactly a thousand names, which is how I should have guessed."
Fix a script that stops at 1,000, step by step
- Confirm the symptom: if your count is exactly 1,000 (or exactly your
MaxKeys), you are reading one page. PrintIsTruncatedfrom the response;trueconfirms it. - Switch to your SDK's paginator (code for each language below), or add the continuation-token loop.
- Handle empty pages, which have no
Contents, so your code does not crash on an empty prefix. - Test on one small prefix first, such as a single month, and compare with the console's count for that "folder."
- Run it on the whole bucket, and if it is slow, split the work by prefix in parallel.
- If you only need totals, switch to CloudWatch's free daily metrics instead, as covered further down.
List all objects in Python (boto3)
The paginator handles the continuation tokens for you. This lists every object under a prefix, however many there are:
import boto3
s3 = boto3.client("s3")
paginator = s3.get_paginator("list_objects_v2")
count, total_bytes = 0, 0
for page in paginator.paginate(Bucket="shop-repair-photos", Prefix="2026/"):
for obj in page.get("Contents", []):
count += 1
total_bytes += obj["Size"]
print(f"{count} objects, {total_bytes / 1e9:.2f} GB")
Two details matter. page.get("Contents", []) rather than page["Contents"]: a page with no objects (an empty prefix, for example) has no Contents key at all, and indexing it raises a KeyError. And the paginator makes calls lazily, one page at a time as you loop, so memory use stays small even for millions of objects.
Just the keys, as a list:
keys = [obj["Key"]
for page in paginator.paginate(Bucket="shop-repair-photos", Prefix="2026/10/")
for obj in page.get("Contents", [])]
The resource interface pages automatically too, and reads a little shorter:
bucket = boto3.resource("s3").Bucket("shop-repair-photos")
for obj in bucket.objects.filter(Prefix="2026/10/"):
print(obj.key, obj.size, obj.last_modified)
Filtering with JMESPath: paginators can filter as they go, for example only objects over 5 MB:
pages = paginator.paginate(Bucket="shop-repair-photos")
for obj in pages.search("Contents[?Size > `5000000`][]"):
print(obj["Key"], obj["Size"])
The filtering still happens on your side; S3 itself can only filter by prefix. Every object is still listed and transferred to you, just discarded afterwards.
Writing the loop yourself (any language)
If you call the API directly, or want to see what the paginator does, the loop is short:
kwargs = {"Bucket": "shop-repair-photos", "Prefix": "2026/"}
while True:
resp = s3.list_objects_v2(**kwargs)
for obj in resp.get("Contents", []):
print(obj["Key"])
if not resp.get("IsTruncated"):
break
kwargs["ContinuationToken"] = resp["NextContinuationToken"]
Keep every other parameter (bucket, prefix, delimiter) identical between calls; only the token changes. Tokens are opaque, so store them as they are, and treat them as short-lived: they are meant for continuing a listing now, not for bookmarking a position for next week. For a resumable position over long periods, save the last key you processed and restart with StartAfter set to it.
List all objects in JavaScript (AWS SDK v3)
import { S3Client, paginateListObjectsV2 } from "@aws-sdk/client-s3";
const client = new S3Client({ region: "us-east-1" });
const paginator = paginateListObjectsV2(
{ client, pageSize: 1000 },
{ Bucket: "shop-repair-photos", Prefix: "2026/" }
);
let count = 0;
for await (const page of paginator) {
for (const obj of page.Contents ?? []) {
count++;
}
}
console.log(count, "objects");
If you prefer to see the loop, the manual version with ListObjectsV2Command follows the same pattern as in Python:
import { ListObjectsV2Command } from "@aws-sdk/client-s3";
let token;
do {
const res = await client.send(new ListObjectsV2Command({
Bucket: "shop-repair-photos", Prefix: "2026/", Delimiter: "/", ContinuationToken: token,
}));
for (const p of res.CommonPrefixes ?? []) console.log("folder:", p.Prefix);
for (const o of res.Contents ?? []) console.log("file:", o.Key, o.Size);
token = res.IsTruncated ? res.NextContinuationToken : undefined;
} while (token);
page.Contents ?? [] plays the same role as Python's .get: empty pages have no Contents. If the client fails before listing anything with "could not load credentials," the problem is credentials, not S3; our JavaScript credentials guide covers that.
List all objects with the AWS CLI
The CLI pages automatically for s3api commands: by default it keeps calling until every object is listed, so list-objects-v2 returns everything, not just 1,000.
# All keys under a prefix, one per line aws s3api list-objects-v2 --bucket shop-repair-photos --prefix 2026/10/ \ --query "Contents[].Key" --output text # Count and total size of everything in the bucket aws s3 ls s3://shop-repair-photos --recursive --summarize --human-readable | tail -n 2 # Only the first 50, with a token to continue later aws s3api list-objects-v2 --bucket shop-repair-photos --max-items 50
Useful options: --max-items caps the total returned and prints a NextToken you can pass back with --starting-token; --page-size changes how many objects each underlying call fetches (smaller pages help when a call times out); --no-paginate makes a single call, the API's raw behavior. The higher-level aws s3 ls is friendlier to read, while s3api gives you every field and the --query filter.
The --query option understands JMESPath, so you can get totals without any other tool:
# Number of objects and total bytes under a prefix aws s3api list-objects-v2 --bucket shop-repair-photos --prefix 2026/10/ \ --query "[length(Contents[]), sum(Contents[].Size)]" # Keys and sizes of objects over 5 MB aws s3api list-objects-v2 --bucket shop-repair-photos \ --query "Contents[?Size > \`5000000\`].[Key, Size]" --output table
On an empty prefix, Contents is missing and these expressions print null rather than zero, which is the CLI's way of saying "nothing here."
List all objects in PowerShell
With the AWS Tools for PowerShell (the AWS.Tools.S3 module), Get-S3Object follows the continuation tokens for you:
Install-Module AWS.Tools.S3 -Scope CurrentUser $objects = Get-S3Object -BucketName shop-repair-photos -KeyPrefix "2026/10/" $objects.Count ($objects | Measure-Object -Property Size -Sum).Sum / 1GB $objects | Where-Object Size -gt 5MB | Select-Object Key, Size, LastModified
Everything is collected into memory before you see it, which is fine for thousands of objects and slow for millions; for very large buckets, use the inventory approach described below. The CLI commands above also run unchanged in PowerShell, which many Windows users find simpler than installing the module.
Mixing the two works well too: let the CLI do the listing and PowerShell do the shaping, by asking for JSON and converting it:
$list = aws s3api list-objects-v2 --bucket shop-repair-photos --prefix "2026/10/" --output json | ConvertFrom-Json
$list.Contents | Sort-Object Size -Descending | Select-Object -First 10 Key, @{n="MB";e={[math]::Round($_.Size/1MB,1)}}
That prints the ten largest photos of October in megabytes, a quick way to spot the uploads that someone forgot to shrink.
Prefixes, delimiters and "folders"
S3 has no real folders. A key like 2026/10/R-1042-before.jpg is a single name that happens to contain slashes; the console just displays it as folders. Two request settings let you work with that structure:
Prefixlimits the listing to keys starting with that text.Prefix="2026/10/"lists October's photos. Include the trailing slash, or2026/1would also match2026/10/,2026/11/and2026/12/.Delimiter="/"groups everything below the next slash intoCommonPrefixes, so you get the "subfolders" instead of every object inside them.
resp_pages = paginator.paginate(Bucket="shop-repair-photos", Prefix="2026/", Delimiter="/")
for page in resp_pages:
for p in page.get("CommonPrefixes", []):
print("folder:", p["Prefix"]) # 2026/01/, 2026/02/, ...
for obj in page.get("Contents", []):
print("file: ", obj["Key"]) # objects directly in 2026/ only
This is how you build a folder browser: list one level with the delimiter, then list a chosen "folder" by using its prefix. Common prefixes count toward the 1,000 per page, so they page too.
Designing keys for listing. Because S3 can only filter by prefix, the way you name objects decides which listings are cheap. Putting the date first (2026/10/19/...) makes "everything from October" one prefix; putting the customer first (acme/2026/...) makes "everything for one customer" one prefix. Pick the order that matches the question you will ask most.
The permission you need: s3:ListBucket
Listing is authorized by s3:ListBucket, and it applies to the bucket, not to the objects. This policy is the classic working pair:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::shop-repair-photos",
"Condition": {"StringLike": {"s3:prefix": ["2026/*", "2026/"]}}
},
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::shop-repair-photos/*"
}
]
}
- Bucket ARN for listing, bucket-slash-star for objects. Putting
s3:ListBucketonarn:aws:s3:::shop-repair-photos/*does nothing, and listing fails with access denied. This mix-up is the most common S3 permission error there is. - There is no
s3:ListObjectsors3:ListObjectsV2action. Both API calls are allowed bys3:ListBucket. - The
s3:prefixcondition limits which "folders" a user may list, handy for giving each customer or team its own prefix. - Versioned buckets need
s3:ListBucketVersionsto list versions.
A side effect worth knowing: without s3:ListBucket, requesting a key that does not exist returns 403 Access Denied instead of 404 Not Found, because S3 will not tell someone who cannot list the bucket whether a key exists. A sudden wave of 403s on missing files often means the list permission was removed, not that the files were.
Counting objects and total size without listing everything
Listing ten million objects takes 10,000 API calls and a long time. When you only need totals or a full catalog, S3 has better tools:
| Tool | What it gives you | Freshness |
|---|---|---|
| CloudWatch storage metrics | NumberOfObjects and BucketSizeBytes per bucket and storage class, free | Once a day |
| S3 console bucket metrics | The same numbers, as charts on the bucket's Metrics tab | Once a day |
| S3 Storage Lens | Organization-wide usage and activity dashboards, with prefix-level detail in advanced tiers | Daily |
| S3 Inventory | A full list of every object (CSV, ORC or Parquet) delivered to a bucket, queryable with Athena | Daily or weekly |
| ListObjectsV2 | The exact current list, with filtering by prefix | Live |
For a dashboard number ("how many photos do we store?"), CloudWatch's daily metric is free and instant. For a full catalog of a huge bucket, S3 Inventory is far cheaper and faster than listing: you get a daily file, and you can query it with SQL in Athena. Live listing is for when you need the current state of one prefix right now.
Setting up S3 Inventory for a very large bucket
When a bucket holds millions or billions of objects and you need a complete list regularly, S3 Inventory writes it out for you instead of you listing it:
- Create (or choose) a destination bucket for the reports, in the same Region. Keeping reports separate from the data keeps them out of your own listings.
- Allow S3 to write there: the destination bucket's policy must let the S3 service (
s3.amazonaws.com) put objects, limited to your source bucket and account. The console offers to add this when you set up the inventory. - Create the inventory configuration on the source bucket: in the console, open the bucket's Management tab, then Inventory configurations, then Create. Choose a prefix or the whole bucket, daily or weekly, and the fields you want (size, last modified, storage class, encryption status and more).
- Pick Parquet or ORC as the format if you will query the reports with Athena; they are smaller and faster to query than CSV.
- Wait for the first report, which can take up to 48 hours to arrive. After that, reports appear on schedule with a manifest file listing their parts.
- Query it with Athena: create a table over the report location, then answer questions in SQL, such as total size per top-level prefix or every object over a size, without a single list call.
Inventory is billed per million objects listed in each report, a small amount for the visibility it gives. For a bucket the size of Jake's, it is overkill; for a company with a hundred million objects, it is the only sane way to know what is in there.
Making big listings faster and cheaper
Cost: LIST requests are billed per 1,000 requests (about half a cent per 1,000 in US East for S3 Standard), and each request returns up to 1,000 keys. Listing a million objects is a thousand requests, around half a cent; listing a billion is around five dollars, before your own compute time. It is rarely the expensive part, but repeated full listings of very large buckets add up, which is another reason to use Inventory.
Speed: a single listing is sequential, because each page needs the previous page's token. To go faster, list several prefixes in parallel. If your keys start with dates, customers or letters, give each worker its own prefix:
from concurrent.futures import ThreadPoolExecutor
def count_prefix(prefix):
pages = s3.get_paginator("list_objects_v2").paginate(Bucket="shop-repair-photos", Prefix=prefix)
return sum(len(p.get("Contents", [])) for p in pages)
months = [f"2026/{m:02d}/" for m in range(1, 13)]
with ThreadPoolExecutor(max_workers=12) as pool:
print(sum(pool.map(count_prefix, months)))
boto3 clients are safe to share across threads for calls like this. Twelve parallel listings finish in roughly the time of the largest one.
Jake: "Twenty-three thousand photos. Is that a lot for S3?"
Ethan: "It's twenty-four list calls. S3 doesn't notice. The bucket could hold a billion and still not notice."
Listing versions and deleted objects
In a versioned bucket, ListObjectsV2 shows only the current version of each object, and hides objects whose latest version is a delete marker. To see every version, including "deleted" files that can still be restored, use ListObjectVersions, which pages with KeyMarker and VersionIdMarker (and its own paginator, list_object_versions):
for page in s3.get_paginator("list_object_versions").paginate(Bucket="shop-repair-photos", Prefix="2026/10/"):
for v in page.get("Versions", []):
print(v["Key"], v["VersionId"], v["IsLatest"])
for d in page.get("DeleteMarkers", []):
print("deleted:", d["Key"], d["VersionId"])
That is also the explanation when the console or a listing says a bucket is "empty" but deleting it fails: old versions and delete markers are still inside. To truly empty a versioned bucket, delete every version and delete marker (the console's Empty button does this), or add a lifecycle rule that expires noncurrent versions and removes expired delete markers, and let S3 clean up over the following days at no request cost.
Deleting many objects: the same 1,000 rule
Deleting has the same page size as listing: DeleteObjects removes up to 1,000 keys per call. The tools handle the batching for you:
- CLI:
aws s3 rm s3://shop-repair-photos/2019/ --recursivelists and deletes everything under the prefix, in batches. Add--dryrunfirst to see what would go. - Python:
bucket.objects.filter(Prefix="2019/").delete()on the resource interface lists and deletes in batches of up to 1,000. - Huge prefixes: a lifecycle rule that expires objects under the prefix is often the best tool for millions of objects. S3 deletes them in the background, and you make no requests at all.
Delete requests themselves are free in S3; the listing needed to find the keys is what you pay for, which is one more reason lifecycle rules win at scale. And on a versioned bucket, a plain delete only adds a delete marker; the data stays (and is billed) until the noncurrent versions are removed too.
S3 limits: object size, object count and key names
| Limit | Value |
|---|---|
| Largest object | 50 TB (since December 2025) |
| Largest single PUT upload | 5 GB; larger objects need multipart upload |
| Multipart upload | Up to 10,000 parts, each 5 MB to 5 GB (the last part can be smaller) |
| Objects per bucket | Unlimited |
| Total storage per bucket | Unlimited |
| Key (object name) length | Up to 1,024 bytes of UTF-8 |
| Keys per ListObjectsV2 page | 1,000 |
| Keys per DeleteObjects call | 1,000 |
Key name tips: letters, digits and ! - _ . * ' ( ) plus / are always safe. Spaces, &, +, = and non-ASCII characters work but need URL encoding in some tools, which is where "file not found" bugs come from: a key with a space may arrive as %20 or + in an event or URL. Backslashes, curly braces, carets and control characters cause the most trouble of all, so leave them out of key names. Our S3 event guide shows the encoding trap in Lambda triggers.
Content type, another common search, is not something listing returns: ListObjectsV2 gives key, size, date, ETag and storage class only. To read an object's Content-Type or custom metadata, call HeadObject on it, one call per object.
Common listing errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
| Exactly 1,000 results | Only the first page read | Use a paginator, or loop on NextContinuationToken |
KeyError: 'Contents' | Empty page or prefix | page.get("Contents", []) |
AccessDenied on ListObjectsV2 | s3:ListBucket missing, or on bucket/* | Allow it on the bucket ARN |
| Nothing listed, but objects exist | Prefix typo, missing or extra slash, wrong case | List without a prefix first and copy a real key's start |
| "Folders" missing from results | Using a delimiter: they are in CommonPrefixes | Read CommonPrefixes as well as Contents |
| Deleted files still counted, or bucket will not delete | Versioning: old versions and delete markers remain | Use ListObjectVersions |
| Listing is very slow | Millions of keys, one sequential listing | Parallelize by prefix, or use S3 Inventory |
NoSuchBucket or a redirect error | Bucket name typo, or client in the wrong Region | Check the name; set the bucket's Region on the client |
| Credentials errors before any listing | No AWS credentials found | See our boto3 credentials guide |
Frequently asked questions
How do I list more than 1,000 objects in S3?
Paginate: repeat ListObjectsV2 with the NextContinuationToken until IsTruncated is false, or use the SDK's paginator, which does it for you.
What is the maximum number of keys ListObjectsV2 returns?
1,000 per call. MaxKeys can be lower but not higher.
How do I list all objects in an S3 bucket with boto3?
Use s3.get_paginator("list_objects_v2").paginate(Bucket=...) and loop over page.get("Contents", []), or bucket.objects.all() with the resource interface.
How do I list S3 objects in JavaScript SDK v3?
Use paginateListObjectsV2({ client }, { Bucket, Prefix }) and a for await loop over the pages.
Does the AWS CLI list more than 1,000 objects?
Yes. aws s3api list-objects-v2 pages automatically and returns everything unless you use --max-items or --no-paginate.
How do I list S3 objects in PowerShell?
Get-S3Object -BucketName NAME -KeyPrefix PREFIX from the AWS.Tools.S3 module, which follows continuation tokens automatically.
What permission is needed to list S3 objects?
s3:ListBucket on the bucket ARN, arn:aws:s3:::bucket-name, not on bucket-name/*. There is no s3:ListObjects action.
What is the maximum object size in S3?
50 TB per object (raised from 5 TB in December 2025). A single PUT can upload up to 5 GB; larger objects use multipart upload.
How many objects can an S3 bucket hold?
An unlimited number, with unlimited total storage.
What is the maximum S3 key length?
1,024 bytes of UTF-8 encoded text.
How do I list folders in an S3 bucket?
Use Delimiter="/" with an optional Prefix. The folders are returned in CommonPrefixes, the files at that level in Contents.
How do I count objects in a large S3 bucket quickly?
Use the free daily CloudWatch metric NumberOfObjects, the bucket's Metrics tab, S3 Storage Lens, or S3 Inventory instead of listing.
What is the difference between ListObjects and ListObjectsV2?
V2 pages with continuation tokens and supports StartAfter; the original uses markers. Both return up to 1,000 keys. Use V2 for new code.
Why does S3 return 403 instead of 404 for a missing object?
Without s3:ListBucket permission, S3 hides whether the key exists and returns Access Denied.
Does ListObjectsV2 return content type?
No. It returns key, size, last modified, ETag and storage class. Use HeadObject for Content-Type and metadata.
How much do S3 LIST requests cost?
They are billed per 1,000 requests, about half a cent per 1,000 for S3 Standard in US East, each returning up to 1,000 keys.
For readers preparing for an AWS certification
We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.
Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026) • Solutions Architect – Associate (SAA-C03) • CloudOps Engineer – Associate (SOA-C03).
Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:
1. A script that lists an S3 bucket always returns exactly 1,000 objects. What is wrong?
It reads only the first page; ListObjectsV2 must be repeated with the continuation token (or an SDK paginator used).
2. Which IAM action allows listing the objects in an S3 bucket, and on which resource?
s3:ListBucket, on the bucket ARN (arn:aws:s3:::bucket-name).
3. A team needs a daily list of all objects in a bucket with billions of objects, queryable with SQL. What do you use?
S3 Inventory reports, queried with Amazon Athena.
4. What is the largest object S3 can store, and the largest single PUT upload?
50 TB per object (since December 2025); 5 GB per single PUT, with multipart upload for larger objects.
5. Where can you see the number of objects in a bucket without listing it?
The free daily Amazon CloudWatch storage metrics NumberOfObjects and BucketSizeBytes (or S3 Storage Lens).
6. Millions of old objects under one prefix must be removed at the lowest cost. What do you use?
An S3 Lifecycle expiration rule for that prefix.
7. A versioned bucket appears empty but cannot be deleted. Why?
Noncurrent versions and delete markers remain; they must be deleted too.
Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.
The fixed script counted 23,481 photos, 61 GB in all, and listed them by month in parallel in about two seconds. Ethan added the bucket's NumberOfObjects metric to the shop's little dashboard so nobody would need the script for the headline number again. Jake looked at the total and asked whether they should delete the oldest ones. "They cost about a dollar fifty a month," Ethan said. "Some of those photos have settled warranty arguments. Keep them."
📌 If you keep one line from this page
ListObjectsV2 returns 1,000 keys per call, so use the SDK's paginator (or loop on NextContinuationToken until IsTruncated is false), filter with Prefix, group with Delimiter, and grant s3:ListBucket on the bucket ARN itself.
Objects up to 50 TB · 5 GB per single PUT · unlimited objects per bucket · keys up to 1,024 bytes · counts: CloudWatch NumberOfObjects.
Revision note. Written October 19, 2026. If a listing returns exactly 1,000 of anything, it is not a coincidence; it is page one.