The graph lives in memory on top of the vectors. A guideline from the FAISS library puts the size per vector at d x 4 + m x 2 x 4 bytes, where d is the number of dimensions. For 10 million vectors of 1,024 dimensions with m = 16, that is 4,096 bytes of numbers plus 128 bytes of links per vector: about 41 GB of vectors, and about 42 GB with the graph, before any stored text. Because HNSW is usually served from RAM, memory is often the largest cost in a self-hosted vector store.
The recall you tuned at launch does not stay put. With the same ef_search, a bigger graph means the walk visits a smaller share of it, so fewer true neighbours come back. An illustrative curve shows a 98 percent recall target needing ef_search of 64 at one million vectors and 256 at ten million, four times the search work per query. Track recall in your monitoring, sampled against exact search on a subset, and tune again as the index grows.
Filters are the other trap. If a store fetches the nearest results and then applies a WHERE filter, a selective filter can return fewer rows than you asked for. Even when the filter is applied during the walk, the vectors that pass it can form disconnected islands in the graph, and the walk can get stuck on one island while the answer sits on another. Qdrant, for example, documents adding extra links so filtered subsets stay connected, and pgvector 0.8 and later can keep scanning with its iterative scan setting.