Fixed
instagram/trending-reels: India serves pages — city-pool geo ladder plus parallel listing renders
country=India had never returned a page (six straight 502 fetch_empty at ~78s over 23-24 Sep). Two root causes: the country-level IN proxy pool cannot finish a headless render of instagram.com/reels at all (systematic read timeout at the full wall, 9/9 across prod and probes), and the old serial listing walk let two of those stalls eat the whole fetch window so the Explore page never even got a try. Indian CITY egress pools render the same India-localized page in 15-17 seconds.
- Listing renders now go out as parallel /reels + /explore/reels pairs (two rounds) — one stalled arm can no longer starve the other
- India rides a city-pool geo ladder: Mumbai egress first, the country pool as the second-round retry; hydration reuses whichever egress actually served the listing
- Verified live: India 7 reels in 23s (was a guaranteed ~78s 502); the same pair design also covers the Turkey-style empty-first-render misses
Fixed
instagram/hashtag-search & reels-search: no more login_wall 502s when the session pool dies — a managed rung serves the same Explore grid
Instagram's login/WAF gate on explore/tags walled off every native tier (four 502 login_wall rows on 24 Sep), and a wall also gates the Decodo rung by design — so the whole ladder died with the session pool. The managed Instagram rung now answers the same hashtag sections feed (top for posts, clips for Reels) when the native listing misses outright. Both hashtag-search and reels-search share the rescue.
- Native listing miss → managed hashtag grid: raw media sections mapped by the exact same code path as a session page
- Media rows arrive complete (author, caption, counts), so the page ships without hopping dead sessions for enrichment
- Verified live with the four failing tags — #trendalert, #reeltrend, #newtrend, #trendingreels: 20 posts each in 14-22s (was 502 login_wall)
Fixed
ad-library/linkedin/search-ads: creative images now fill and ad formats are named honestly — videos are no longer 'Single Image Ad'
LinkedIn lazy-loads SERP creatives: the real media.licdn URL sits in a data-delayed-url attribute while src holds a placeholder ghost. The extractor read only src, so most cards shipped with an empty media array even though the images were on the page (one audited SERP carried 48 delayed URLs against 16 hydrated src). Separately, any card with media was labelled 'Single Image Ad' — including video posters and document pages, whose CDN paths name the creative kind.
- SERP extraction reads data-delayed-url before src — live media fill went from near-zero to 20/20 (microsoft/US) and 22/24 (Forex/DE)
- adFormat derives from the creative URL shape: videocover → Video Ad, ads-document-cover → Document Ad, multiple images → Carousel Ad; the same inference backs /linkedin/ad-details when the page carries no About-the-ad label (LinkedIn's own label still wins)
- advertiser/cta/paidForBy stay null on search rows by design — the SERP card does not carry them; hydrate via /linkedin/ad-details as the platformNote says
Fixed
ad-library/linkedin/search-ads: limits > 25 no longer 503 when the browser render stalls — a concurrent plain fetch serves an honest partial page
After the plain-first ladder shipped, five more 503s appeared with one shape: all on limits > 25, all at exactly ~88s. The grown browser render had stalled to its full 84s wall, leaving 11s — too little for the retry pair (needs 30s) and too little for the serial plain rescue (a plain fetch takes 3-12s, it got 8). The plain fetch now starts at t0 beside the browser render, so its first ~25 cards are in hand by ~15s no matter what the render does.
- Grown requests (limit > 25) start a cheap no-JS fetch in parallel with the browser render; it is discarded when the render lands
- When every browser render misses, the pre-fetched plain page ships the first ~25 cards with an honest hasMore=true instead of a 503
- Verified live against the exact failing shape: Forex/DE limit=48 with all three browser renders down — 24 ads in 83.5s (was a guaranteed 88s 503)
Improved
ad-library/linkedin/search-ads: 3-10s answers for limits ≤ 25 — the first page is served without a browser render
The LinkedIn Ad Library search page turns out to be fully server-rendered: a plain proxy fetch returns the first ~25 ads in 3-12 seconds, while the headless browser render we used for every request took 25-80s and carried both remaining failure modes (degraded-pool session misses and the stall that eats the retry window — two more 503s on 24 Sep even after the parallel retry). The browser is now only used to grow the page past 25 cards.
- limit ≤ 25 (the default): plain fetch first, ~3-10s typical; the parallel headless pair only runs if it misses — three independent chances, no serial-stall gap
- limit > 25: unchanged grown ladder, plus a new last-resort plain fetch that ships the first ~25 cards as an honest partial page (hasMore=true) instead of a 503
- A partial rescue that can't reach a requested offset window stays a retryable miss — never a fake isLastPage
- Verified live: 20 ads in 6.6s, 12 in 9.1s, zero-hit in 2.3s, grown 40-ad page in 30.8s
Fixed
instagram: channel-posts pagination, tagged-posts and highlights survive Instagram session-pool death via a managed fallback
When our Instagram session cookies die (roughly weekly), the timeline feed 401s on every proxy tier, the tagged feed hits a login wall and the highlights tray rate-limits — a public profile answered 502 on the next feed page, a 90s timeout 503 on tagged posts, and a definitive-sounding 422 on highlights that actually had six live albums. All three surfaces now fall back to the same managed Instagram pool that already rescues channel-reels and comments, returning identical raw payloads through the existing mappers.
- Timeline feed pages (channel-posts pagination), the tagged feed and the highlights tray each gained the managed rung; public cursors pass through unchanged
- The tagged-posts native tier walk is time-boxed (dead sticky sessions burned ~2m45s of redirect loops — past the route deadline before any fallback could run)
- Tagged-posts profile resolve skips the session pool (same reasoning as highlights: dead-pool redirect loops burned ~72s of the 90s deadline)
- Author-stats enrichment is budgeted — when the pool is dead you get lean posts instead of a 503
- Verified live on the reported profile: next feed page 12 posts, 15 tagged posts, all 6 highlight albums with titles
Fixed
facebook/details: photo permalinks (photo.php?fbid=…) are posts now, not "Invalid Facebook URL"
A photo permalink 400'd as an invalid URL: the classifier knew permalink.php and story.php but not photo.php?fbid=, /photo/?fbid= or /page/photos/set/id — and the details parser only looked for Story nodes while photo pages embed a Photo node instead. Both layers now understand photos: the same single render serves caption, author, reactions, comments and the full-size image, and the feedbackId works with /v1/facebook/comments.
- All three photo permalink forms classify as posts and pass the details gate
- The Photo node's container story supplies the caption, author and canonical post URL — no second render needed
- A photo id that doesn't match any node on the page stays an honest retryable miss, never another photo's data
- Bonus: share links that land on photo URLs now resolve instead of failing as unresolved
Fixed
ad-library/linkedin/search-ads: the retry now runs two proxy sessions in parallel — a degraded-pool window needs three independent misses to 503
During a degraded proxy-pool window (24 Sep 10:55–11:01 UTC) LinkedIn search answered three ~80s 503s interleaved with 200s of the very same query — the misses are per-session random, not systemic, yet the ladder retried with a single session and one more unlucky draw ended the request. The retry round now renders through two parallel sessions and serves whichever paints the library, cutting the joint-miss rate quadratically. The extra render is only spent when the first attempt already missed.
- Verified live: a first-attempt miss on a heavy advertiser recovered via the parallel pair — 20 ads in 87.5s where the single-session retry had answered nothing
- When both arms paint, the one with more ads wins; failures remain 0-credit and retryable
- Residual: a first attempt that stalls to its full 80s wall still can't fit a retry inside the 95s deadline — that rare case stays an honest retryable 503
Fixed
ad-library/tiktok/search: zero-result queries could 503 when the first render missed — TikTok's own empty answer is now honored from every rescue arm
When the first-page render missed, the pagination rescue could capture TikTok's own definitive answer — code 0, zero ads — and still throw it away, because zero rescued rows never beat an empty pool. Five zero-hit queries burned ~82 seconds each and answered 503 upstream_unavailable in one afternoon, while identical queries minutes later answered an honest empty 200 in 15 seconds. A code-0 empty answer from either rescue arm is now always a 200. The rescue round also runs both capture modes in parallel (they fail independently), so a 503 now requires three independent capture misses instead of two sequential ones.
- Verified live on all five failing queries: four zero-hits answered empty 200 in 13–19s, the fifth served 24 rows — none returned 503
- Rescue page-1 rows ship with honest hasMore instead of being dropped when pagination misses
- The extra parallel render costs only on the miss path; the direct JSON API stays fenced off (TikTok's per-request x-ccl-str signature answers 421 to every replay — verified across datacenter, residential, and direct egress)
Fixed
youtube/audio-transcript: videos whose audio-only URLs 403 now transcribe via the muxed stream — and the rescue ladder finally fits its budget
Some videos sign audio-only stream URLs that answer 403 on every egress, while the combined audio+video stream from the very same player response downloads fine. The extractor treated the 403 as a dead end, burned its whole 20-second budget walking fallback clients that couldn't help, and answered 502 audio_extract_failed — the yt-dlp rescue never got a turn. The native rung now falls back to the muxed stream (video is stripped during the speech re-encode), the extract budget grew to 45s sized against measured ASR times under the 110s route deadline, and the fallback rungs must leave the yt-dlp rescue a reserved slice instead of starving it.
- Verified live on the failing video: audio-only 403 → muxed stream 200 in 2s, full 77s of audio transcribed end-to-end in 22s
- Interrupted yt-dlp downloads (.part files) can no longer be served as if they were the full audio
- Failures remain 0-credit; the 502 now only fires when every rung — including yt-dlp — actually ran and missed
Fixed
facebook/profile-posts: heavy pages could still 503 — the two light hops no longer share one deadline
After the tab-first ladder reorder, both light hops (first-paint home, then the /posts tab) ran under the same 22s deadline. Production showed renders of heavy pages like nasa spiking past 22s on both hops in the same request (listing phase 44s), landing a 503 that the failure marker then replayed fast. The home hop now gets 20s, the /posts tab hop gets its own 28s window sized to what's left of the budget, and old failure markers written before the wider window are versioned away.
- Home first-paint 20s + /posts tab 28s fits the 55s listing budget with margin for the details fetch
- Failure-marker cache key bumped so pre-fix fast-503s can't outlive the fix
- Deadline arithmetic is unit-tested against the observed 12–19s (calm) and 22s+ (spiky) render times
Fixed
tiktok/popular-songs: TikTok removed the public song chart — the error now says so, and rediscovery burns dropped from every 5 minutes to hourly
TikTok's One Creative Suite migration removed the public Creative Center song chart: the music page now redirects to a trends hub with only hashtag/video/creator tabs, the chart API answers 40101 no-permission to anonymous callers, and the extended actor scraping the same surface returns nothing (all verified 24 Sep). The endpoint already refused to substitute For You sounds; it now names the removal explicitly and remembers the outage for an hour instead of five minutes, so at most one 20–30s rediscovery probe runs per hour while auto-recovery stays in place should TikTok restore the chart.
- 503 CREATIVE_CENTER_UNAVAILABLE message now explains the chart was removed in the TikTok One migration (retryable: false)
- Persistent negative cache extended 5min → 1h — prod had burned eight 20–34s scrapes across 22–24 Sep rediscovering the same removal
- Last-good chart pages still serve as stale 200s when cached; all failures remain 0-credit
Fixed
ad-library/linkedin/search-ads: retry grants now leave room for a stalled render — no more 113s overruns
A stalled proxy render can run ~25s past its granted budget (read drain + wall cap). The retry attempt granted up to 55s whenever 30s remained on the clock, so a shell-page miss followed by a stalled retry walked one request to 113s against the 95s deadline before answering 503. Every attempt's grant now subtracts the stall overshoot from the time remaining, so the ladder mathematically cannot exceed its deadline.
- Root cause from prod 24 Sep: a 251-byte proxy shell at ~33s, then a full 55s retry grant whose stall burned 80s
- Attempts too small to fit a render plus its overshoot are skipped instead of started
- Same two-attempt retry contract — one blip still never 503s a search
Fixed
ad-library/tiktok/search: limit>12 asks 503'd when the first-page render stalled — a pagination rescue now serves the rows
Asking for more than 12 TikTok ads ran two 35-second first-page render attempts; when the proxy pool was slow, each stall overran its grant and together they ate the whole budget, then the route declared the library down (503) even though the pagination capture path was healthy in the same minute. The ladder now spends at most one tight ~15s probe on the first page and, when that misses, hands the remaining budget to the pagination capture as a rescue instead of giving up.
- Verified live: a first-page miss now recovers via pagination — 24 rows in 44s where the old ladder 503'd after 72s
- Zero-hit answers from TikTok's own API are still an honest empty 200, never a rescue
- Failures remain 0-credit and retryable
Fixed
spotify/podcast-episodes: pages came up one short and the cursor re-served rows
The episode archive interleaves RestrictedContent stubs (geo-blocked episodes) among real rows — Joe Rogan's page 1 opens with one. The stubs are unusable and get dropped, which shorted the page (limit=3 → 2 rows), and the offset cursor advanced by rows returned rather than archive entries consumed, so the next page re-fetched — and could re-serve — rows around the stub. Pages now backfill from the next archive slice to fill the requested limit, and cursors advance by entries actually consumed.
- Dropped RestrictedContent stubs are backfilled — limit=3 returns 3 episodes again
- nextCursor advances by archive entries consumed: no duplicates, no skipped episodes across pages
- Verified live on JRE: pages 1–2 are consecutive (#2557…#2552) with zero overlap
- Response-cache version bumped so short cached pages cannot serve
Fixed
reddit/search & post-comments: one hung upstream request failed live queries
Both endpoints rode a single scrape-pool attempt with a tight timeout. The pool answers reddit JSON in 2–5s normally, but an individual request occasionally hangs well past any sane cap — that lone coin-flip turned into 503s on /search (the 6s attempt died) and 502s on /post-comments (the 15s comments.json fetch ReadTimed-out on a live thread). Both paths now retry once with a fresh request, which reliably lands.
- search: two scrape attempts budgeted inside a raised deadline (12s → 25s); direct-fetch rungs unchanged
- post-comments: comments.json fetch retries once (15s + 12s) after the authoritative existence probe
- Verified locally 3/3 on both paths against the previously failing thread and query
Fixed
facebook/profile-reels: small limits could answer noPublicReels: true for profiles with public reels
A single degraded page render that resolved no reel ids, combined with an actor miss, was treated as proof that the profile has no public reels — a 200 with noPublicReels: true and zero rows. Absence-of-evidence is not evidence: the affirmative 200 now requires either the actor's explicit empty answer or a two-render native confirmation. Anything less stays a retryable 5xx at 0 credits.
- noPublicReels: true requires affirmative evidence (actor-confirmed empty or double native confirm)
- Degraded single-render zero-id results no longer masquerade as an empty profile
Fixed
instagram/channel-reels: high-limit asks answered 2 rows with a wrong-flavored cursor
When the native listing missed, a web-profile-info timeline fallback served whatever videos sat in the profile's ~12-post timeline edge — sometimes just 2 — and canceled the richer scrape/actor race that was still running. A limit=25 ask got 2 rows and a posts-style cursor. The thin timeline result is now held as a last-resort partial instead of served eagerly: the full ladder keeps running, and the thin set ships (flagged partial) only if every richer rung misses.
- Timeline fallback serves immediately only when it can fill min(limit, 12) rows
- Thinner sets are kept as a partial-of-last-resort while the scrape/actor race finishes
- Served thin sets are flagged partial: true with a wpi_thin stage marker
- Response-cache version bumped so cached 2-row pages cannot serve
Fixed
facebook/profile-posts: heavy pages (nasa) 503'd every run — cheap /posts tab now runs before the 40s scrolled render
The listing ladder tried a scrolled home-page render as its second hop. On heavy pages that render mostly dies in a ~40s ReadTimeout and eats the whole 55s budget, so the third hop — a light /posts tab render that actually paints post permalinks in ~19s — never ran. nasa returned 503 after ~53s on every attempt, and the failure marker then served fast 503s for the cache window. The /posts tab hop now runs second, and the light-hop deadline was raised from 16s to 22s to cover measured 12–19s first paints.
- Hop order is now: first-paint home → light /posts tab → scrolled home (last, only if time remains)
- Light-hop deadline 16s → 22s — real first paints of heavy pages were being cut off mid-render
- Verified live: nasa now answers 200 with hydrated post data in ~20s instead of 503 at 53s
- Response-cache and failure-marker versions bumped so pre-fix 503 markers cannot fast-serve
Fixed
instagram/comments: SESSION_UNAVAILABLE on full-thread asks — managed logged-in comments now serve the page
Comments beyond Instagram's 2-row logged-out preview depended on our own session-cookie pool, whose cookies die roughly weekly — when the pool was down, every limit>2 request answered 503 SESSION_UNAVAILABLE. A managed logged-in rung (HikerAPI /v2/media/comments) now pages the same raw comment shape with no cookies of ours: ~15 rows per hop with a real resume cursor. The session pool still runs first when alive; the 503 remains only for the rare case where every rung misses.
- Full comment threads page again during session-pool outages — verified live: 15 rows + working second page on an 820-comment post
- New cursor form {mediaPk}:h:{pageId} so managed pagination can never be mistaken for a session resume token
- Existing session cursors are honored by the pool only — the managed rung won't restart page 1 under a foreign cursor (no duplicates)
- timings.hikerMs exposes the new rung; failures stay 0-credit and retryable