Listing & pagination
Listing walks the keys in a bucket. This page covers both clients, the delimiter and common-prefix model, and full paging loops.
- The native client has
listObjectswith prefix/delimiter semantics and an iterator that follows continuation tokens. - The S3 client has two forms:
ListObjectsV2(the token-paged default) andListObjects(the older marker-paged v1), plus paginators that follow the tokens for you.
For the operation matrix, see the native data-plane reference and the S3 operations reference.
One page
A single listing call returns one page. prefix filters by key prefix; maxKeys caps the page (the server caps it at 1000); the page reports isTruncated and a nextContinuationToken when more remains.
const page = await native.listObjects("reports", { prefix: "q1/", maxKeys: 100 });
for (const o of page.objects) console.log(o.key, o.size);
console.log(page.isTruncated, page.nextContinuationToken);page, err := native.ListObjects(ctx, lockwellnative.ListObjectsInput{
Bucket: "reports", Prefix: "q1/", MaxKeys: 100,
})
for _, o := range page.Objects {
fmt.Println(o.Key, o.Size, o.ETag)
}
fmt.Println(page.IsTruncated, page.NextContinuationToken)// ListObjectsOptions record: prefix, delimiter, continuationToken, maxKeys.
import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;
var page = native.listObjects("reports", new ListObjectsOptions("q1/", null, null, 100));
page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
System.out.println(page.isTruncated() + " " + page.nextContinuationToken());The S3 ListObjectsV2 returns the same page shape:
const page = await s3.listObjectsV2("reports", { prefix: "q1/", maxKeys: 100 });
for (const o of page.objects) console.log(o.key, o.size, o.etag);
console.log(page.isTruncated, page.nextContinuationToken);page, err := s3.ListObjectsV2(ctx, "reports",
lockwellsdk.WithPrefix("q1/"), lockwellsdk.WithMaxKeys(100))
for _, o := range page.Objects {
fmt.Println(o.Key, o.Size, o.ETag)
}
fmt.Println(page.IsTruncated, page.NextContinuationToken)// listObjectsV2(bucket, prefix, maxKeys, continuationToken)
var page = s3.listObjectsV2("reports", "q1/", 100, null);
page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
System.out.println(page.truncated() + " " + page.nextContinuationToken());Listing options
| Option | Native (Go / Node / Java) | S3 (Go / Node / Java) |
|---|---|---|
| Key prefix | Prefix / prefix / ListObjectsOptions.prefix | WithPrefix / prefix / arg 2 |
| Delimiter | Delimiter / delimiter / ListObjectsOptions.delimiter | WithDelimiter / delimiter / v1 form |
| Start after a key | StartAfter (Go) / startAfter / startAfter on the wire | WithStartAfter / startAfter |
| Continuation token | ContinuationToken / continuationToken / arg 3 | WithContinuationToken / continuationToken / arg 4 |
| Max keys per page | MaxKeys / maxKeys / ListObjectsOptions.maxKeys | WithMaxKeys / maxKeys / arg 3 |
startAfter begins the listing after a given key (a one-shot starting point, not a resume token). continuationToken resumes a previously truncated listing.
Pass startAfter or continuationToken, not both. They serve different purposes: one sets a starting key,
the other resumes a paged listing. :::
Delimiters and common prefixes
A delimiter (almost always "/") makes the listing behave like a directory walk. Keys that share a prefix up to the next delimiter are collapsed into a single entry in commonPrefixes; only keys with no further delimiter past the prefix appear in objects.
Given these keys:
logs/2026/01/a.log
logs/2026/01/b.log
logs/2026/02/c.log
logs/index.txta listing with prefix: "logs/" and delimiter: "/" returns:
objects:logs/index.txtcommonPrefixes:logs/2026/
To descend, list again with the common prefix as the new prefix. Without a delimiter, the listing is flat: every key under the prefix comes back in objects.
const page = await native.listObjects("archive", { prefix: "logs/", delimiter: "/" });
console.log(page.commonPrefixes); // ["logs/2026/"]
for (const o of page.objects) console.log(o.key); // "logs/index.txt"page, _ := native.ListObjects(ctx, lockwellnative.ListObjectsInput{
Bucket: "archive", Prefix: "logs/", Delimiter: "/",
})
fmt.Println(page.CommonPrefixes) // ["logs/2026/"]
for _, o := range page.Objects {
fmt.Println(o.Key) // "logs/index.txt"
}import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;
// Native carries a delimiter directly:
var page = native.listObjects("archive", new ListObjectsOptions("logs/", "/", null, null));
System.out.println(page.commonPrefixes());
page.objects().forEach(o -> System.out.println(o.key()));::::
const page = await s3.listObjectsV2("archive", { prefix: "logs/", delimiter: "/" });
console.log(page.commonPrefixes); // ["logs/2026/"]
for (const o of page.objects) console.log(o.key); // "logs/index.txt"page, _ := s3.ListObjectsV2(ctx, "archive",
lockwellsdk.WithPrefix("logs/"), lockwellsdk.WithDelimiter("/"))
fmt.Println(page.CommonPrefixes) // ["logs/2026/"]
for _, o := range page.Objects {
fmt.Println(o.Key) // "logs/index.txt"
}// The S3 v1 listObjects carries the delimiter as its last argument:
var page = s3.listObjects("archive", "logs/", null, 1000, "/");
System.out.println(page.commonPrefixes());
page.objects().forEach(o -> System.out.println(o.key()));Paging across all keys
To walk every key, follow isTruncated with the continuation token until it clears. The native client ships an iterator that does this for you; the S3 client ships paginators per listing operation.
The native iterator (ListObjectsAll in Go, the paginateObjects async generator in Node) transparently follows continuation tokens.
for await (const page of native.paginateObjects("reports", { prefix: "q1/" })) {
for (const o of page.objects) console.log(o.key, o.size);
}it := native.ListObjectsAll(ctx, lockwellnative.ListObjectsInput{Bucket: "reports", Prefix: "q1/"})
for it.Next() {
o := it.Object()
fmt.Println(o.Key, o.Size)
}
if err := it.Err(); err != nil {
return err
}// Page the native listing with the continuation token:
import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;
String token = null;
do {
var page = native.listObjects("reports", new ListObjectsOptions("q1/", null, token, null));
page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
token = page.isTruncated() ? page.nextContinuationToken() : null;
} while (token != null);The S3 client ships an auto-pager for each listing operation, so you never thread a continuation token by hand. The loop shape is the same in every language: ask whether more pages remain, fetch the next page, repeat.
// Node uses async iterators rather than a paginator object:
for await (const page of s3.paginateObjectsV2("reports", { prefix: "q1/" })) {
for (const o of page.objects) console.log(o.key, o.size);
}p := s3.NewListObjectsV2Paginator("reports", lockwellsdk.WithPrefix("q1/"))
for p.HasMorePages() {
page, err := p.NextPage(ctx)
if err != nil {
return err
}
for _, o := range page.Objects {
fmt.Println(o.Key, o.Size)
}
}// The S3 client exposes paginators for versions, multipart uploads, and parts.
// For a flat object listing, page with the continuation token:
String token = null;
do {
var page = s3.listObjectsV2("reports", "q1/", 1000, token);
page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
token = page.truncated() ? page.nextContinuationToken() : null;
} while (token != null);The Go paginators (NewListObjectsV2Paginator, NewListObjectVersionsPaginator, NewListMultipartUploadsPaginator, NewListPartsPaginator) own the tokens. Seed them with the same options the one-shot method takes, but do not pass a continuation/marker yourself: the paginator manages it.
Paging by hand
If you would rather drive the loop yourself, follow isTruncated with the continuation token until it clears.
let token;
do {
const page = await native.listObjects(
"reports",
token ? { prefix: "q1/", continuationToken: token } : { prefix: "q1/" },
);
for (const o of page.objects) console.log(o.key);
token = page.isTruncated ? page.nextContinuationToken : undefined;
} while (token);var token string
for {
in := lockwellnative.ListObjectsInput{Bucket: "reports", Prefix: "q1/", ContinuationToken: token}
page, err := native.ListObjects(ctx, in)
if err != nil {
return err
}
for _, o := range page.Objects {
fmt.Println(o.Key)
}
if !page.IsTruncated || page.NextContinuationToken == "" {
break
}
token = page.NextContinuationToken
}import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;
String token = null;
do {
var page = native.listObjects("reports", new ListObjectsOptions("q1/", null, token, null));
page.objects().forEach(o -> System.out.println(o.key()));
token = page.isTruncated() ? page.nextContinuationToken() : null;
} while (token != null);let token;
do {
const page = await s3.listObjectsV2(
"reports",
token ? { prefix: "q1/", continuationToken: token } : { prefix: "q1/" },
);
for (const o of page.objects) console.log(o.key);
token = page.isTruncated ? page.nextContinuationToken : undefined;
} while (token);var token string
for {
opts := []lockwellsdk.ListOption{lockwellsdk.WithPrefix("q1/")}
if token != "" {
opts = append(opts, lockwellsdk.WithContinuationToken(token))
}
page, err := s3.ListObjectsV2(ctx, "reports", opts...)
if err != nil {
return err
}
for _, o := range page.Objects {
fmt.Println(o.Key)
}
if !page.IsTruncated || page.NextContinuationToken == "" {
break
}
token = page.NextContinuationToken
}String token = null;
do {
var page = s3.listObjectsV2("reports", "q1/", 1000, token);
page.objects().forEach(o -> System.out.println(o.key()));
token = page.truncated() ? page.nextContinuationToken() : null;
} while (token != null);ListObjects v1 (marker pagination)
The S3 client also exposes the v1 ListObjects, which pages by a marker (the last key returned) rather than an opaque token. Prefer ListObjectsV2 (or the native listing) for new code; v1 exists for parity with clients that still page by marker.
let marker;
do {
const page = await s3.listObjects("reports", marker ? { prefix: "v1/", marker } : { prefix: "v1/" });
for (const o of page.objects) console.log(o.key);
marker = page.isTruncated ? page.nextMarker || page.objects.at(-1)?.key : undefined;
} while (marker);page1, _ := s3.ListObjects(ctx, "reports",
lockwellsdk.WithPrefix("v1/"), lockwellsdk.WithMaxKeys(2))
page2, _ := s3.ListObjects(ctx, "reports",
lockwellsdk.WithPrefix("v1/"), lockwellsdk.WithMaxKeys(2),
lockwellsdk.WithMarker(page1.NextMarker))// listObjects(bucket, prefix, marker, maxKeys, delimiter)
var page1 = s3.listObjects("reports", "v1/", null, 2, null);
var page2 = s3.listObjects("reports", "v1/", page1.nextMarker(), 2, null);When a delimiter collapses the truncation boundary onto a common prefix, the server may omit NextMarker. In that case resume from the last key or common prefix you saw, which is what the Node and Go paginators do for you.
Listing versions and multipart uploads
Versioned listings (ListObjectVersions) and in-progress multipart uploads (ListMultipartUploads, ListParts) share this prefix/delimiter/marker model. They each have a dedicated page:
- Versioning: list versions and delete markers.
- Multipart uploads: list uploads and their parts.
Next steps
- Upload & download: the put/get/head you are listing.
- Copying objects: duplicate keys you found in a listing.
- Deleting objects: batch-delete a listed prefix.