I run a visa agency in Bali. The other half of my week goes into Apify Actors, and one of them reads Craigslist: you give it cities, a category and a price range, and it returns listings with a numeric price, coordinates, square feet, attributes and the full description.
The part of it I did not understand until I had three weeks of data is the word newest.
My own health check hits the Actor with the same input every day:
{
"cities": ["sfbay"],
"category": "apa",
"sort": "newest",
"maxItems": 50,
"includeDetails": true,
"dedupeCrossPosts": true
}
Apartments for rent, SF Bay Area, 50 rows, newest first. Between 22 October and 11 November 2026 that exact input ran 42 times and delivered 1,965 listing rows. Everything below is from those rows.
Two runs six hours apart shared zero listings
On 11 November the Actor ran at 04:05 UTC and again at 10:31 UTC. Both returned 50 rows. The intersection of their posting IDs is zero.
That is not a scraper bug and not a cache miss on my side. Every single one of the 50 rows in the 10:31 run has a bump timestamp later than the start of the 04:05 run — 50 of 50. The two windows do not touch:
| run | bump times covered |
|---|---|
| 04:05 UTC | 02:43 → 03:59 |
| 10:31 UTC | 08:33 → 10:25 |
So "the 50 newest" is a sliding window about 76 and 112 minutes wide in those two runs. Across all 42 runs the median width is 42 minutes. The narrowest was 24 October at 00:18 UTC — 16:18 local in California, the busiest hour of the Bay Area posting day — where the 50 newest rentals covered 3 minutes.
Two runs 16 minutes apart at 02:15 local kept 41 of 50 rows. Runs a few seconds apart are byte-identical. The churn rate is not a constant; it tracks the local clock in the city you asked about.
"Newest" means bumped, not posted
Here is the part that changes how you use the data. In that 10:31 run, 7 of 50 listings were actually posted that day. The median row was 12.6 days old. The oldest was posted on 13 October — 28.6 days before it showed up in a feed labelled newest.
Across all 1,965 rows:
| rows | |
|---|---|
| posted within 24h of the run | 1,233 (63%) |
| older than 7 days | 467 |
| older than 14 days | 286 |
| oldest single row | 29.96 days |
That ceiling is not a coincidence. Craigslist expires postings after about 30 days, so the oldest thing that can appear in "newest" is an ad on its last day alive. Mine was a Santa Clara 2-bedroom at $3,981, posted 22 September, sitting in the newest feed on 22 October.
Why are old ads there at all? Because the sort key is the last touch, not the creation date. The Actor exposes both: postedAt comes from the posting's recordCreated, updatedAt only appears when the posting was edited more than a minute after it was created.
Of the 732 rows older than one day, 730 carried an updatedAt. Two did not. For the rows that have one, the gap between the edit and my run was a median of 17.6 minutes, and 686 of 740 were edited within the hour before the run.
Read that as a rule: an old ad enters the newest feed only because a human or a management tool touched it minutes ago. Property managers re-list constantly. One ad in my data — a template-titled complex listing, "Balcony or Patio, Cable Ready, Access to Public Transportation", $3,660 — was posted on 7 October, appeared as "newest" on 22 October, and appeared again on 5 November. Twenty-nine days after it was written, it was still the newest thing on Craigslist twice.
How fresh the feed is depends on the hour you run it
Grouping all 1,965 rows by the local California hour of the run, the share posted within the last 24 hours:
| local hour (PST) | genuinely new | rows |
|---|---|---|
| 02:00 | 13% | 100 |
| 07:00 | 40% | 178 |
| 08:00 | 75% | 150 |
| 12:00 | 64% | 1,040 |
| 16:00 | 83% | 400 |
| 20:00 | 34% | 50 |
At 2 a.m. local time nobody is posting, so the 50 newest slots fill with bumps of old inventory. At 4 p.m. five out of six rows are real new supply. If your schedule runs at a fixed UTC hour and you never looked at postedAt, you have been measuring the bump habits of apartment complexes and calling it market activity.
598 of 1,965 rows were a second delivery of something I already had
dedupeCrossPosts works inside a run: it uses Craigslist's own dedupe key plus a title+price signature. Across all 1,965 rows I found only 3 where the same title and price arrived under two different posting IDs — and all three were suspiciously cheap re-posts filed under a new ID within the hour (two of them $880 and $951 in a market whose median row here is $2,844).
Between runs, nothing dedupes for you. 598 of the 1,965 rows (30%) were an ID I had already been delivered in an earlier run; 467 distinct ads came back more than once, one of them eight times. On a pay-per-row Actor that is 30% of the bill for data you already had.
Two things fix it. The Actor has an onlyNewRowsStore option — give it a name and each run only delivers (and only charges for) IDs it has not seen before. Or keep your own set of IDs and dedupe on arrival. Either way, do not dedupe on content: of the 467 ads I saw more than once, exactly one changed its price across three weeks, from $2,905 to $2,910. Re-reading the same ad is not price tracking.
Calling it
One search request plus one request per listing for details — 51 HTTP requests for the run above, no proxy, done in 32 seconds. Via the API:
curl -s -X POST \
"https://api.apify.com/v2/acts/lergassy~craigslist-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'content-type: application/json' \
-d '{"cities":["sfbay"],"category":"apa","sort":"newest","maxItems":50,"includeDetails":true}'
A trimmed real row:
{
"type": "listing",
"id": "7990869072",
"title": "2 bedroom, Located in San Jose, Backsplashes",
"price": 3595,
"priceText": "$3,595",
"neighborhood": "los gatos",
"address": "1029 Meridian Ave, San Jose, CA 95125",
"latitude": 37.30562,
"longitude": -121.913932,
"postedAt": "2026-10-27T19:10:10.000Z",
"updatedAt": "2026-11-11T10:25:40.000Z",
"bedrooms": 2, "bathrooms": 2, "sqft": 940,
"imageCount": 9
}
Field coverage over the 1,965 rows, so you know what to expect: bedrooms, latitude, longitude, description and attributes on 100%, images on 98%, address on 94%, sqft on 85%, a numeric price on 97%. The Actor is lergassy/craigslist-scraper.
One oddity I cannot fully explain: a pair of runs 12 minutes apart on 28 October also shared zero IDs, but only 35 of the later 50 had been bumped in between, and one row in it was last touched six days earlier. Craigslist's search endpoint takes a cache id and a cache timestamp, so I assume I was handed two different cache slices. It does not change the practical advice — never treat row position as meaningful, dedupe on id.
When this is the wrong tool
- You want a price history of one apartment. One $5 move in 467 repeat observations. Craigslist ads are re-posted, not re-priced.
- You want contact details. Craigslist relays them; the listing does not show them, so no row carries them.
-
You want "everything posted today". Ask for more rows than you need and filter on
postedAtyourself, or setpostedToday. Sorting by newest and taking 50 gives you the last 42 minutes of bumps, which is a different thing. -
You want to monitor hourly. Use
onlyNewRowsStoreor you will pay for the same complex re-listing itself all week.
Everything else — rent comps across a whole metro, supply counts by neighborhood, sourcing leads in ten cities at once — holds up fine, as long as the date you join on is postedAt.
Top comments (0)