Engineering Decisions
The Pipeline That Looked Fine
- Call the external API for candidates matching the query.
- Enrich each one - a separate API call per profile, ~8 seconds each.
- Ingest into our corpus.
- Score against the original query.
- Append to the result set.
- Update the result count for the client.
The Real Cost
What Streaming Actually Looks Like
For each batch of 3-5 candidates:
enrich → ingest → score → append → bump resultCount
The client does not change. The same /results?searchId=X endpoint that was already polling now sees the count rising and the list growing every five seconds. The first batch lands in roughly 25 to 40 seconds instead of 90 to 120. By the time the recruiter has read the first three candidates, three more have appeared.
There is no infrastructure change here. Same workers, same database, same external API. The only difference is where the commit boundary sits.
Three Discovery Tiers, Run In Parallel
Dedup Is Not an Afterthought
Two Layers of Defense
Before any external API call, the worker loads knownUsernames for the search - every external identifier already attempted, regardless of outcome. The query going to the API is filtered against this set. The stub row for the candidate is written before the enrichment call so future workers see them as known. Database-level uniqueness.
The (searchId, candidateId) pair has a unique constraint. Inserts use ON CONFLICT DO NOTHING. If the pre-call dedup misses - and it will, sometimes, under concurrent ingestion - the database catches it without raising. The order matters. Doing the unique-constraint check first and the in-memory check as a hot-path optimization gives correctness regardless of how the workers happen to be scheduled.
What Goes Wrong Without Retry
Why retry only on 5xx and timeouts?
What about 429s?
Why bound the retry to 1?
Per-Candidate Failures Don't Block the Search
What This Looks Like Operationally
- Append-only result rows. Once a candidate is in the result set, they do not move. Pagination depends on this.
- Server-set rank. The Engine assigns rank as candidates flow in. The frontend never re-sorts.
- Status machine on candidates. queued → running → succeeded | failed_*. The frontend renders by status; failures are not hidden.
- Stale-search recovery. If a worker crashes, the row is marked failed. If the entire search goes silent for ten minutes, an idle reaper marks it stale and lets the user retry.