DEV Community

Dodo Data
Dodo Data

Posted on

How do you know a job posting closed? 30% of the closures we first recorded were wrong

A job board is judged by what it stops showing. A listing that stays up after the employer filled the role wastes an applicant's evening, and nothing in a public careers API tells you it happened: there is no closed event. A job is there on Monday and missing on Tuesday, and you have to decide what that means.

We keep a daily index of open jobs from about 11,800 company career sites on seven systems, Greenhouse, Ashby, Lever, Personio, SmartRecruiters, Teamtailor and Recruitee: 511,622 open jobs on 4 November 2026. Last week we started recording closures from it, before Teamtailor and Recruitee joined on 4 November, so every closure below is from the other five. The obvious rule is one line:

closed = jobs in yesterday's list that are not in today's
Enter fullscreen mode Exit fullscreen mode

Between 30 October and 1 November 2026 that rule, with one guard already in place, recorded 19,811 closures. 5,979 of them, 30%, were wrong. Every number below is from those runs and the ones that followed.

Wrong answer 1: the job never left, your read of it did

The guard we started with: a job closes only when its company's board was read successfully in that refresh. A timeout, a 500 or a rate limit tells you nothing about the jobs on that board, so they stay open until the next read. A board that answers 404 or lists nothing is different: that is a real statement, and every job on it closes. The refresh of 2 November read 9,695 boards in full and found 1,022 gone.

That guard was not enough, because a read can succeed and still be incomplete.

A review of our code on 31 October took 20 random SmartRecruiters ids we had just called closed and asked the API about each one. 10 of the 20 were active: true. Of the 2,673 SmartRecruiters jobs closed on 30 October, 820 were back in the index the next day: 31%, against 17 of 1,822 on all the other systems together.

The cause is offset paging over a list that changes while you read it. Every page answered with a 200. But when postings are added and removed mid-read, offsets shift, so some postings appear twice and others never appear. Reading Domino's gave 24,684 rows over 247 pages, and those rows held only 23,376 distinct jobs. 749 of its 1,956 "closures" that day were live.

The fix uses a number the API already gives you, the total it says it holds:

// Every page answered, but fewer distinct jobs than the board itself reports.
const outcome = expected > 0 && distinct < expected ? 'partial' : 'read';
// 'read' and 'gone' can close jobs. 'partial' and 'failed' close nothing.
Enter fullscreen mode Exit fullscreen mode

It has a cost, and it is better to say so: a job that a partial board really closed that day is not reported until a later full read. 11 boards were partial on 2 November, 7 on 3 November. We prefer missing a closure for a day over telling a customer to delete a live job.

Wrong answer 2: the job was re-posted under a new id

Some employers re-post at scale. Domino's does it daily: the same role, the same place, the same requisition number, a new id. About half of the 2,429 closures on 1 November had a job with the same company and title first seen that same day.

This one is wrong twice. It reports a closure that did not happen, and it reports a new job that is not new, which is about 1,900 false "new" rows a day if you sell a new-jobs feed, as we do.

So a closed job is now paired, one to one, with a job first listed in the same refresh on the same board under the same title, place and requisition number. A matched pair is not a closure, and the new id inherits the old first-seen date. The refresh of 3 November matched 3,287 of them.

Then we went back: the records for 30 October to 1 November were cleaned of the 5,979 jobs that had been listed again or re-posted, with a backup first. That is where the 30% comes from.

The match is deliberately strict, and strict has a cost as well. On 2 November one job we checked by hand had been re-posted under a new id with a longer title. It counted as closed and new. On 3 November 641 new listings carried the title of a job the same board closed that day, without the same place or requisition number. We leave those as they are, because guessing would hide real replacements.

Wrong answer 3: you closed something you know nothing about

A job carried over from a partial or failed read has no row in the previous snapshot. On 3 November 1,829 such ids were missing from the new list. We could have reported them closed, with no title and no company, and no way to tell whether they had been re-posted. A closure you cannot describe or check is not a closure, so now they are not reported.

Checking the answer

None of this means anything without a sample against the employer's own board, on the same day:

  • 2 November: 87 closures checked (20 per system, all 7 on Personio). 86 were gone. The other is the re-post with the longer title.
  • 3 November: 9,461 closures that day, 160 checked. Greenhouse 60 of 60 and Lever 40 of 40 answered 404. On SmartRecruiters 54 of 60 were closed, and the other 6 were still active by id but absent from a listing search of that board.

Those last 6 are why our definition is careful: closed means the employer's board no longer lists it, not that the role was filled. We can see the listing. We cannot see the hire.

The volume also moves more than you would guess, so one day's count tells you little. The refreshes from 29 October to 2 November closed 16,380, 2,566, 9,622, 1,644 and 1,687 jobs, and 3 November closed 9,461.

What we would tell you before you build one

  1. Treat a missing row as a question. Ask whether the read was complete before you ask whether the job is gone.
  2. Compare distinct ids with the total the API reports. A paged read that answers 200 on every page can still be wrong, on the largest boards most of all.
  3. Match re-posts before you count anything, in both directions: closed and new are the same mistake seen from two sides.
  4. Key on the id, not the URL. On Greenhouse about one closure in four carries the employer's own careers address rather than a Greenhouse one.
  5. Keep a backup and be ready to correct the past. We rewrote three days of records once we knew better.

If you would rather not build it, the result is the Closed Jobs Feed on Apify: one row per job a career site stopped listing, with the same id as Career Site Jobs Search and Career Jobs Feed, so you can delete exactly what you took. Every job closed by the refresh of 2 November (1,687) cost about $0.19 to fetch.

Dodo Data is a small job-data shop. Claude, Anthropic's AI, does the engineering, including the bugs described here and the review that found them.

Top comments (0)