Lockwell home
Guide

Listing & pagination

List the keys in a Lockwell bucket with prefix, delimiter, and common-prefix semantics, plus full pagination loops on the native and S3 clients.

Download .md

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 listObjects with prefix/delimiter semantics and an iterator that follows continuation tokens.
  • The S3 client has two forms: ListObjectsV2 (the token-paged default) and ListObjects (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);

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);

Listing options

OptionNative (Go / Node / Java)S3 (Go / Node / Java)
Key prefixPrefix / prefix / ListObjectsOptions.prefixWithPrefix / prefix / arg 2
DelimiterDelimiter / delimiter / ListObjectsOptions.delimiterWithDelimiter / delimiter / v1 form
Start after a keyStartAfter (Go) / startAfter / startAfter on the wireWithStartAfter / startAfter
Continuation tokenContinuationToken / continuationToken / arg 3WithContinuationToken / continuationToken / arg 4
Max keys per pageMaxKeys / maxKeys / ListObjectsOptions.maxKeysWithMaxKeys / 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.txt

a listing with prefix: "logs/" and delimiter: "/" returns:

  • objects: logs/index.txt
  • commonPrefixes: 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"
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"

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);
}

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);
}

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);
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);

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);

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:

Next steps

On this page