Every new product starts with the same diagram: an API, a database, and then a queue, a search cluster and a vector store, each with its own dashboard and its own pager. Most teams add those boxes before they need them.
This issue is about the opposite move: asking the database you already run to do one more job, and knowing exactly when to stop.
The default stack
Four services means four upgrade paths, four sets of credentials and four ways to lose a write. None of that shows up on the architecture slide. It shows up at 3 a.m., when a job was enqueued but the row it points to was rolled back.
Queues with SKIP LOCKED
Postgres has shipped row-level locking that skips rows already taken by another worker for years. That is all a job queue needs: workers that never pick the same job twice, and no extra service to babysit.
-- claim one job, safely, from many workers
select * from jobs
where status = 'queued'
order by created_at
for update skip locked
limit 1;At our volume, about 40 jobs a second at peak, this ran for eight months with no incidents. The limit we hit first was not the queue but our own retry logic.
The fastest service to run is the one you never deploy.
Full-text search
A generated tsvector column and a GIN index cover search for most products: stemming, ranking, highlighting, several languages. You lose learned relevance. For a docs site or an admin panel, nobody notices.
Vectors next door
With pgvector, embeddings live in the same transaction as the rows they describe. Deleting a document deletes its vector. There is no sync job to forget about.
When to leave
- Write volume that needs partitioning you don't want to own.
- Search that has to rank like a search engine.
- A team that already runs the specialised tool well.
Until then: one box, well understood, beats four boxes nobody fully owns.
Member discussion
Become a member of Offset to join the conversation.