Last updated: August 2026. pgvector and Postgres versions move fast, so recheck any numbers you generate every few months.
Introduction
A lot of teams turn on pgvector, load in a few thousand test rows, see fast results, and assume that’s how it’ll behave forever. In reality, it doesn’t. As the table grows, query latency (how long it takes to get search results back) doesn’t move in a straight line, and most teams don’t find that out until it’s already a production problem. This article explains why that happens and how to find your own breaking point, instead of borrowing someone else’s benchmark numbers, which won’t match your hardware, your data, or your query pattern anyway.
Before getting into that, it helps to be clear on what’s actually being stored and searched.
What an embedding is: An embedding is a list of numbers that represents the meaning of something: a sentence, an image, a product description. An embedding model reads the input and outputs that list. For example, the sentence “the cat sat on the mat” might turn into something like:
[0.021, -0.384, 0.117, 0.098, ... ]
That list is usually a few hundred to a few thousand numbers long (common sizes are 384, 768, and 1536). A sentence about a dog sitting somewhere would turn into a different list of numbers, but a list that sits close to the cat sentence’s numbers, because the two sentences mean similar things. A sentence about tax law would land far away from both.
Why this is needed: Computers can’t compare meaning directly. Regular text search matches exact words or phrases. Embeddings solve a different problem: they turn meaning into numbers, so “how similar are these two things” becomes “how close are these two lists of numbers.” That’s what makes semantic search, recommendations, and RAG (retrieval-augmented generation) possible: finding results that mean the same thing, not just share the same words.

How the vectors get stored: This is where pgvector comes in. It adds a vector column type to a normal Postgres table, so each row can hold its regular data plus its embedding, side by side. For example:
CREATE TABLE documents (
id serial PRIMARY KEY,
content text,
embedding vector(1536)
);
Every row in that table stores its own fixed-length list of numbers in the embedding column. Searching means asking Postgres: “find the rows whose numbers are closest to this list of numbers.” That single idea, comparing lists of numbers for closeness, is what everything else in this article is about, and it’s also where the slowdowns start once the table gets big.
The short version
pgvector’s query latency doesn’t grow up slowly as your table grows. It stays flat for a long time, then jumps hard at a specific point.
That jump isn’t really about row count. It’s caused by three things:
- Which index you use (IVFFlat or HNSW)
- Whether your index still fits in memory. Once it gets too big for RAM, Postgres has to read parts of it from disk instead, which is much slower
- Whether you’re tuned for speed or for recall
This article won’t hand you a number for where that jump happens on your table, because it depends on your hardware, your dimension size, and your data. What it will do is explain the mechanism clearly enough that you can find your own breaking point, and show you exactly how to test for it.
What pgvector actually is
pgvector is a Postgres extension. It is not a separate database.
It adds a vector column type to Postgres, plus two ways to search that column quickly:
- IVFFlat: splits your vectors into clusters (“lists”). A search only checks a few clusters instead of every row.
- HNSW: builds a graph that connects similar vectors. A search walks the graph instead of scanning the whole table.
This choice matters more than anything else in this article. Most of what happens to latency as your table grows comes down to which of these two you picked.
There’s an important default to know before any of that, though:
- Without an index, pgvector checks every row and compares it directly. This is called exact nearest neighbor search, and it always returns the correct, complete result: 100% recall, every time.
- Adding an index (IVFFlat or HNSW) switches you to approximate nearest neighbor search. It’s much faster, but it trades away some accuracy to get there.
- That tradeoff has a side effect worth knowing: with most database indexes, adding one doesn’t change your results, just how fast you get them. With an approximate vector index, that’s not true. The same query can return slightly different results once an index is added, and even different results across repeated runs.
Important pgvector syntax
This section covers the actual SQL behind everything described above, so the rest of the article isn’t just theory.
Turn on the extension: This only needs to run once per database:
CREATE EXTENSION IF NOT EXISTS vector;
Add a vector column: The number in parentheses is the dimension. It must exactly match the output size of whatever embedding model you’re using (1536 for many OpenAI models, 384 or 768 for common open-source models). A table can’t mix embeddings from two models with different dimensions in the same column.
CREATE TABLE documents (
id serial PRIMARY KEY,
content text,
embedding vector(1536)
);
Insert a vector: pgvector accepts a vector as a plain text list of numbers:
INSERT INTO documents (content, embedding)
VALUES ('the cat sat on the mat', '[0.021, -0.384, 0.117, ...]');
Search for the closest matches: This is exact (no index) search, ordering rows by distance and keeping the closest 5:
SELECT content FROM documents
ORDER BY embedding <-> '[0.02, -0.39, 0.11, ...]'
LIMIT 5;
The distance operators. pgvector gives you three ways to measure “closeness,” and the one you use should match what your embedding model recommends:
| Operator | Name | When to use it |
|---|---|---|
<-> |
L2 (Euclidean) distance | General-purpose default; straight-line distance between two points |
<=> |
Cosine distance | Most common for text embeddings (OpenAI and many others recommend this); compares direction, ignores magnitude |
<#> |
Negative inner product | Useful when magnitude carries meaning, and slightly cheaper to compute if vectors are already normalized |
Creating an HNSW index: m controls how many connections each point in the graph gets; ef_construction controls how thorough the graph-building search is. Higher values on both improve recall but increase build time and memory:
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Note the vector_cosine_ops part: the index has to be built with the same distance operator you plan to query with (vector_l2_ops, vector_cosine_ops, or vector_ip_ops), or Postgres won’t use the index at all.
Creating an IVFFlat index: lists controls how many clusters the data gets split into. Unlike HNSW, IVFFlat needs real data in the table before you build it, since it clusters based on what’s actually there:
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Tuning search behavior at query time: These settings don’t change the index. They change how hard each search works against it, trading speed for recall:
SET hnsw.ef_search = 100; -- for HNSW
SET ivfflat.probes = 10; -- for IVFFlat
These six pieces of syntax cover everything referenced earlier in this article, and everything you’d need for the “find your own breaking point” test later on.
What causes the jump
Three mechanisms explain almost everything about how pgvector behaves as a table grows. None of them depend on a specific benchmark. They’re just how the underlying pieces work.
It comes down to memory: HNSW keeps its entire graph in RAM while it’s being used. As long as the graph fits in memory, lookups stay fast and predictable. Once the graph outgrows available memory, Postgres has to pull pieces of it from disk instead, and disk reads are far slower than memory reads. This is why latency doesn’t rise slowly. It holds steady right up until the graph no longer fits, then it steps up all at once.
IVFFlat degrades unevenly, not gradually: IVFFlat splits vectors into clusters and only searches a handful of them per query. If the data is spread evenly across clusters, this stays fast. But real-world embeddings usually aren’t spread evenly. Some topics or categories are far more common than others. That unevenness means some clusters get overloaded while others stay small. A query that lands in an overloaded cluster is slow. A query that lands in a small cluster is fast. So instead of a steady slowdown, you get random spikes.
Distance to storage adds up: If the Postgres instance and the application or embedding service calling it aren’t close together, network time gets added on top of whatever the index itself costs. At small scale, that overhead is invisible next to everything else. At scale, when the index is already working harder, that added distance compounds instead of staying constant.
None of this is a flaw specific to pgvector. Any index-based search system has some version of this ceiling. pgvector just makes the tradeoffs visible earlier, because it’s running inside a general-purpose database rather than a system built only for vector search.
Why index builds take so long?
Query latency usually isn’t the first thing that breaks. Index build time is.
Building an index means Postgres has to look at every existing vector and figure out how it relates to the others: clusters for IVFFlat, graph connections for HNSW. That work grows with the size of the table, and for HNSW it also grows with how thorough you tell it to be (its m and ef_construction settings). A denser, more accurate graph takes longer to build.
This matters most for teams doing continuous ingestion rather than a one-time load. Every time the index gets rebuilt, that cost happens again, and teams often notice this cost long before they notice any change in query speed.

You can tune for speed or for accuracy, not both
Speed and accuracy go against each other, and pgvector makes you choose:
- HNSW’s
ef_searchand IVFFlat’sprobesboth control how hard the index searches before returning results - Turning either one up improves recall but increases latency
- Turning either one down does the opposite
So “is pgvector fast enough” isn’t really the right question. The real question is: fast enough, at what recall, for your data. That’s a setting you tune for your own workload, not a fixed number that transfers from someone else’s table.
When pgvector still makes sense (and when it doesn’t)
pgvector is a good fit when:
- Your table is small enough that the index still fits comfortably in memory
- You’re already running Postgres and don’t want to run a second system
- Your recall requirements are moderate, not extreme
- Your ingestion is batch-based, not constant
A dedicated vector database (Qdrant, Milvus, Pinecone, or similar) is worth considering when:
- Your table has grown well past what fits in memory on a reasonably sized instance
- You need strict latency guarantees under high query volume
- Your data changes constantly and index rebuild time is a real operational cost
- You need built-in horizontal scaling, since Postgres wasn’t built for that
Neither answer is universally correct. It depends on where your table sits today, and where it’s headed over the next year.
What changes the curve
A few levers push these thresholds further out, without switching systems:
- Quantization: storing vectors as half-precision or binary values instead of full floats. This shrinks both index size and memory footprint, which delays the point where the index outgrows RAM. It costs some recall, and that cost should be measured on your own data before you rely on it.
- Right-sizing memory: an instance with enough RAM to hold your HNSW graph avoids the disk fallback that causes the latency jump described above.
- Separating build from write traffic: rebuilding indexes during low-traffic windows instead of continuously.
- Keeping pgvector current: recent versions have shipped real improvements to build speed and index size. Version upgrades are often a bigger lever than query-level tuning.
How DigitalOcean Managed PostgreSQL implements this
DigitalOcean Managed PostgreSQL for vector search runs on the same managed Postgres engine used across DigitalOcean’s Managed Databases product, with pgvector enabled at the database level. A few operational details are worth knowing if you’re setting this up on that platform specifically.
Extensions are scoped per database, not per cluster, so pgvector has to be enabled separately in every database where vectors will be stored. There’s also a naming detail that trips people up: the project is called “pgvector,” but the extension itself registers under the name vector. Running CREATE EXTENSION pgvector; fails with an error. The correct command is the same one used earlier in this article:
CREATE EXTENSION IF NOT EXISTS vector;
pgvector is supported on PostgreSQL 13 and later. Index creation follows the standard syntax covered in the syntax section above: HNSW and IVFFlat both work as documented, and DigitalOcean’s own guidance recommends HNSW as the default for most small-to-medium workloads, with IVFFlat reserved for cases where build time or memory matters more than recall.
How to find your own breaking point
The setup is straightforward:
- Establish a baseline: Pick a set of test queries, maybe 50 to 100, and run each one against your table with no index at all. This is the exact nearest neighbor search described earlier, so the results are guaranteed correct. Save those results. They become the answer key you’ll check everything else against, so this step matters more than it looks like it does.
- Pick size checkpoints: Decide on a few table sizes to test along the way, based on how your table is actually expected to grow. If you have 200,000 rows today and expect to hit 5 million within a year, reasonable checkpoints might be 200K, 1M, and 5M. Testing at sizes you’ll actually reach is more useful than testing at round numbers picked at random.
- Build the index at each checkpoint: At each size, build the index fresh (don’t just keep adding to an old one) and write down how long the build takes. This is worth tracking on its own, since build time is often the first thing to become a real problem, well before query speed does.
- Run the same set of queries at each checkpoint: Use the exact same test queries from step 1, but now with the index in place, and record how long each one takes. Don’t just average the times. Also check the slower end, usually called p95 or p99, meaning the time it took for the slowest 5% or 1% of queries. Averages can look fine while a meaningful chunk of real users are seeing something much slower.
- Compare results against your baseline: For each test query, check how many of the results from step 4 match the “true” results you saved in step 1. The percentage that match is your recall at that table size. Doing this at each checkpoint shows you whether recall is holding steady or slipping as the table grows.
- Watch for the jump, not a slope: Once you have latency, recall, and build time recorded at each checkpoint, look at how they change from one size to the next. You’re not looking for a steady, gradual increase. You’re looking for the specific size where a number suddenly jumps compared to the step before it. That size is your real answer, and it’s the one number in this whole article worth writing down, because it’s the only one that’s actually about your table.
This only takes a Postgres instance with pgvector enabled, a set of real or representative embeddings, and a script that runs the six steps above. It’s a small project, and it’s the only way to get a number that’s actually true for your table.
Conclusion
pgvector works fine for a long time. Then it slows down fast, usually because its index no longer fits in memory, not because the table slowly got bigger. That point is different for every table, so a number from this article won’t help you much. What helps is running your own test: search with no index to get a baseline, then check speed, accuracy, and build time at a few table sizes, and watch for the size where something changes fast instead of slow. If you reach that point, tools like pgvectorscale can push it further out before you need a separate vector database.
References
- pgvector GitHub repository and README: official source for syntax, index types, operators, and configuration options
- PostgreSQL documentation: Extensions: background on how Postgres extensions like pgvector work
- DigitalOcean: How to Enable pgvector
- DigitalOcean: How to Create a Vector Index
- DigitalOcean: How to Use pgvectorscale with PostgreSQL Vector Search
- DigitalOcean: Best Practices for Advanced PostgreSQL Vector Workloads