Ask
117
@pgpolicy_nadia ·

cost per active user hit $1.90 on a $9 plan after adding pgvector search Unit Economics

Note-taking tool for researchers, $9/mo flat, 640 monthly actives. Infra was $310/mo and comfortable. Then I shipped semantic search: chunk every note, embed with a hosted embedding model, store in Postgres with pgvector, HNSW index.

Infra is now $1,216/mo. Rough split:

  • embeddings API: $402
  • Postgres compute: $611 (was $180)
  • storage + everything else: $203

That is $1.90 per active user against $9 of revenue, before payment fees, and my heaviest 5% of users are obviously much worse than the average. Gross margin went from ~87% to ~79% overall but the tail is what worries me.

The embedding cost is because I re-embed a note on every save. People save constantly. A single note gets re-embedded 30 times in an afternoon while someone edits it.

The Postgres number I genuinely do not understand. The instance was fine before and the query volume did not change much.

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @back_of_envelope · 2mo ago · 3 replies

    Re-embedding on every save is the whole $402 and it's the easiest thing on your list to fix.

    Chunk first, hash each chunk, store the hash next to the vector. On save, re-chunk, and only send chunks whose hash changed. Someone editing paragraph 4 of an 11-paragraph note re-embeds one chunk, not eleven. Our re-embed volume dropped 94% and the quality is identical because the unchanged text produces the identical vector anyway.

    Second, debounce. There is no reason to embed on keystroke-save. Queue it, coalesce, run 30 seconds after the last edit. Combined with hashing that took us from ~180k embed calls a day to about 6k.

    Third, check whether your embedding model supports shortened output dimensions. If it does, going from 1536 to 768 halves your index size and your index build time with a small retrieval quality hit that is usually invisible for a notes product. Measure it on your own data before you commit, obviously.

    88
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @pgpolicy_nadia · 2mo ago

      Chunk hashing is obvious in hindsight and I feel silly. Question: when a chunk boundary shifts because someone inserted a sentence early in the note, does that not invalidate every downstream chunk anyway?

      22
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @packlist_pen · 2mo ago

      It does with naive fixed-size chunking, which is why content-defined boundaries help - split on headings or paragraphs rather than every N tokens. Then an insert in paragraph 2 only dirties paragraph 2. Worst case you fall back to re-embedding a section, not the document.

      17
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @egress_egon · 2mo ago

    Cache the query embeddings too, not just the document ones. In our logs about 38% of searches in any given week are a string we have embedded before - people re-run the same search, or five users in a team search the same project name. A hash-keyed cache with a 30 day TTL is twenty lines and shaves real money off the API line.

    37
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @kraut_corner · 2mo ago · 2 replies

    The Postgres jump is almost certainly index maintenance, not queries. HNSW insert is expensive - every new vector walks the graph and updates neighbour lists. If you are re-embedding 180k chunks a day you are doing 180k HNSW inserts a day on top of your normal write load, and the CPU for that is not free.

    Turn on pg_stat_statements and look at total_exec_time ordered desc. My bet is you see the INSERT/UPDATE on the vectors table well above any SELECT.

    Other things worth checking:

    • maintenance_work_mem during index build. If it is too small the build spills and takes forever, burning compute the whole time.
    • whether you are rebuilding the index rather than incrementally inserting. A full HNSW build on a few million vectors with default m=16, ef_construction=64 is hours of pinned CPU.
    • your autovacuum settings. High-churn vector tables bloat fast and nobody notices until the instance is 3x the size it should be.

    Fix the re-embed volume and the Postgres bill probably fixes itself as a side effect.

    63
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @durable_ines · 2mo ago

      Adding to this: if you ever do need a full rebuild, bump maintenance_work_mem for that session specifically and drop it back after. We went from 4h to 38m on 2.1M vectors by giving the build 8GB instead of the default. Just do not leave it high globally or a few concurrent autovacuums will OOM the box.

      26
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @integral_ines · 2mo ago

    Engineering answers aside, you also have a pricing problem. "$9/mo, unlimited semantic search over unlimited notes" is a promise where your cost scales with usage and your revenue does not. That is fine at 640 users and fatal at 6,400.

    You do not need usage-based billing tomorrow. You need a limit that is generous enough that 95% never see it and real enough that the tail cannot ruin you. Something like 2,000 indexed notes on the $9 plan. Ship it for new signups, grandfather everyone existing, and now your worst case is bounded.

    25
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @first_repo_finn · 2mo ago · 2 replies

    Honestly for notes, tsvector full text search is free, runs on the same box, and handles maybe 90% of what people actually search for. The vector stuff is a lot of cost for the last 10%.

    11
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @packlist_pen · 2mo ago

      Different jobs. Full text cannot find "the thing about the leaky pipeline" when the note says "funnel drop-off between signup and activation". But you are half right in a way that saves money: run tsvector first, take the top 50 candidates, and only rerank those semantically. Cuts embedding-side work a lot and retrieval quality goes up because lexical matches on exact terms stop getting buried.

      9
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @corwin_ashby · 2mo ago

    $1.90 CPAU on a $9 plan is survivable. The version of this post I was expecting had a $3 plan.

    6
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report