Amazon S3 Vectors for Storing Embeddings
9 min read · updated August 11, 2026
S3 Vectors is a distinct bucket type with its own API surface — s3vectors, not s3 — and two of its design decisions are fixed at index creation. Getting those right first is most of the work.
Bucket, index, vector
Three nested things. A vector bucket is a bucket type optimised for vector storage; it is not a general purpose bucket and you do not put objects in it. Inside it live vector indexes, each with a fixed dimension and distance metric. Inside those live vectors: a key, an array of floats, and optional metadata.
Amazon announced general availability of S3 Vectors on 2 December 2025, having previewed it earlier that year, with an increase in scale at GA — the documented ceiling is now up to two billion vectors per index and 10,000 indexes per vector bucket. The API model dates from 2025-07-15, which is the service version string the SDKs carry.
Creating the index
Two calls, and the second one contains the decisions you cannot revise:
aws s3vectors create-vector-bucket --vector-bucket-name corpus-vectors
aws s3vectors create-index \
--vector-bucket-name corpus-vectors \
--index-name documents \
--data-type float32 \
--dimension 1024 \
--distance-metric cosine \
--metadata-configuration '{"nonFilterableMetadataKeys": ["sourceText"]}'dataType accepts one value, float32. dimension ranges from 1 to 4,096, so check your embedding model’s output width against that ceiling before committing — a model emitting wider vectors cannot be stored without reduction. distanceMetric accepts euclidean or cosine and nothing else; if your model is trained for inner-product similarity, normalise the vectors to unit length and use cosine, which is equivalent on normalised inputs.
nonFilterableMetadataKeys is the field to think hardest about. Metadata splits into filterable and non-filterable, with separate budgets: total metadata per vector up to 40 KB, of which filterable metadata may be at most 2 KB, with up to 50 keys per vector and up to 10 non-filterable keys per index. Anything you want to store but never filter on — above all the chunk’s source text, which is what makes the result useful to a model — belongs in the non-filterable list, where it does not consume the tight 2 KB budget. Leave it out and a few hundred words of text will exhaust the filterable allowance on its own.
Getting this wrong means rebuilding the index and re-embedding the corpus, which is the expensive part. Decide the list before the first PutVectors call.
Writing vectors
import boto3, hashlib
s3v = boto3.client("s3vectors")
def index_chunks(doc_id, chunks, embeddings):
vectors = []
for i, (text, vec) in enumerate(zip(chunks, embeddings)):
vectors.append({
"key": f"{hashlib.sha256(doc_id.encode()).hexdigest()}:{i}",
"data": {"float32": vec},
"metadata": {
"docId": doc_id, # filterable
"tenant": "acme", # filterable
"chunkIndex": i, # filterable
"sourceText": text, # non-filterable, declared at create
},
})
# Up to 500 vectors per call; 20 MiB request payload.
for batch in (vectors[i:i+500] for i in range(0, len(vectors), 500)):
s3v.put_vectors(
vectorBucketName="corpus-vectors",
indexName="documents",
vectors=batch,
)A deterministic key is what makes the write idempotent: PutVectors replaces a vector with the same key, so a document delivered twice by an S3 event notification produces the same set of keys and overwrites rather than duplicates. Since S3 event delivery is at-least-once, this is not an optimisation.
Two throughput ceilings bound the ingest side: AWS documents up to 1,000 combined PutVectors and DeleteVectors requests per second per index, and up to 2,500 vectors inserted and deleted per second per index. Note that those two do not scale together — batching to the 500-vector maximum saturates the vector rate at five requests per second, so the request-rate ceiling is generous only if you batch poorly. A large backfill is bounded by the 2,500-per-second number, and at that rate a ten-million-chunk corpus is about an hour of continuous writing.
Querying with a filter
resp = s3v.query_vectors(
vectorBucketName="corpus-vectors",
indexName="documents",
queryVector={"float32": query_embedding},
topK=8,
filter={"tenant": {"$eq": "acme"}},
returnMetadata=True,
returnDistance=True,
)
for match in resp["vectors"]:
print(match["key"], match["distance"])
print(match["metadata"]["sourceText"][:200])topK may go up to 10,000, with up to 100 results per page in the response — so a large topK means pagination, not one big answer. For RAG you want a small number anyway: the point of the retrieval step is to fit useful context into a prompt, and eight good chunks beat a hundred mediocre ones both in quality and in what you pay for input tokens.
The filter operates on filterable metadata only, which is the second half of the decision you made at index creation. Tenant isolation, document type, recency buckets — anything you will narrow by has to be filterable and has to fit in 2 KB alongside everything else filterable. Keep the values short: an ISO date string is a fine filterable value, a full document title is a waste of a scarce budget.
On latency, Amazon stated at GA that infrequent queries return in under a second and more frequent queries see latencies around 100 milliseconds or less, and that an index can serve hundreds of QueryVectors requests per second. That is a vendor statement, not a measurement made here, and the shape of it — faster when queried more often — tells you something real about the design: this is storage-backed rather than memory-resident, so a cold index pays for that on the first query. If your application needs a hard single-digit-millisecond p99, this is the wrong tier and OpenSearch or a dedicated vector database is the right one.
The limits that shape the design
- 10,000 vector buckets per Region per account, 10,000 indexes per bucket. Ample for an index per tenant if you want hard isolation, which is worth considering as an alternative to a
tenantfilter on one shared index. - Up to 2 billion vectors per index. Not the constraint you will hit first.
- 40 KB total metadata per vector, 2 KB filterable, 50 keys. The 40 KB total is what bounds how much source text you can co-locate with a vector — roughly ten thousand words, which is more than any sensible chunk.
- 500 vectors per
PutVectorsorDeleteVectors, 100 perGetVectors, 20 MiB request payload. At 1,024 dimensions a float32 vector is about 4 KB, so 500 of them is roughly 2 MB — the count limit binds well before the payload limit unless your metadata is heavy. - Errors worth handling by name.
TooManyRequestsException(429) means back off — retry it with jitter.ServiceQuotaExceededException(402) means you hit a ceiling, and retrying will not help.ConflictException(409) onCreateIndexmeans the name is taken, which in a provisioning script usually means the resource already exists and the script should proceed.
The decision this page cannot make for you is whether to run the pipeline at all. S3 Vectors is the storage layer; if you would rather not own the chunking, embedding and sync logic above it, a Bedrock knowledge base over an S3 bucket does that work and gives up this level of control over keys, metadata and filters.