Dev Investigation Notes
When Playwright page.goto times out after 30 seconds — the networkidle trap
This is a translated article. Read the original in Korean ↗
The rendering pipeline for personaverse, a project featuring AI characters bickering in short-form videos, opened HTML pages in headless Chromium and recorded the screen with Playwright. After swapping in a 1080x1620, 53-second background video and starting a render, the 109MB clip caused the page-opening step (page.goto) to time out after 30 seconds, killing the render.
Conclusion
page.goto(url, waitUntil="networkidle") considers loading complete when "network requests have been quiet for a while." But when a page keeps downloading a large video, that condition can diverge from whether it is actually ready to render. In this case, too, waiting for the full 109MB clip to download kept networkidle from meeting its condition within 30 seconds. Puppeteer's networkidle0/networkidle2 judge loading completion the same way—network connections must stay at zero or no more than two for a set period—so the same cause produces the same symptom.
The fix was to replace network idle as the loading criterion with domcontentloaded plus an explicit "ready" signal set by the page itself. In the session, record.py was changed to wait only for the DOM to load instead of waiting for networkidle, then leave the rest to polling __ready.
# Before — the networkidle condition was not met within 30 seconds while downloading a large video
page.goto(url, wait_until="networkidle")
# After — once the DOM loads, use a flag set by the page to determine actual render readiness
page.goto(url, wait_until="domcontentloaded")
page.wait_for_function("window.__ready === true")
With this pattern, window.__ready must not be set at an arbitrary moment. The page's script must explicitly set it to true when the background video is actually playable—the video element's canplay/loadeddata event—or when the DOM being recorded has finished rendering. If you leave out this condition, wait_for_function never passes, and you eventually hit another timeout.
Situation
personaverse is a project that makes short-form videos of AI characters with generational backgrounds, family histories, and ties to modern Korean history bickering over issues. The engine opens rendering HTML in headless Chromium and records the screen with Playwright. That architecture allowed background video to be added as an HTML <video> layer rather than composited separately.
The episode being made used speech bubbles from the character Park Doeun over a montage to explain a twist: someone who seemed to be singing as a joke turned out to be a good singer. The user brought an edited video and explained that it started with praise from Liv, moved into a scene of playful singing, and ended with a scene of genuinely good singing .
Symptom
The clip was a vertical video with audio, a resolution of 1080x1620, and a duration of 53 seconds. After analyzing the timeline and adding Park Doeun's dialogue to the scene configuration, the first render failed immediately. The diagnosis confirmed in the session was that "the clip was large at 109MB, so page.goto could not satisfy networkidle (waiting for the full download) within 30 seconds and timed out".
Cause
record.py was using page.goto(url, waitUntil="networkidle") to open the rendering page. This condition treats the point when the network goes quiet as the completion of page loading. But the key asset on this rendering page was a large background video played within the page. While downloading the 109MB clip, networkidle could not meet its completion condition within 30 seconds, causing page.goto to time out.
The core problem was less that the clip was "large" than that render readiness was being judged by "network idle." In a screen-recording pipeline, checking whether the HTML structure is ready and the page is actually ready to render is a more direct condition.
Fix
The waitUntil option on page.goto was changed from networkidle to domcontentloaded. Actual render readiness was then determined by polling a custom __ready flag set by the page itself, rather than by checking network state
. Re-running the render with the same clip immediately after the change successfully produced a 53.5-second output within the 60-second limit.
That conclusion held through the rest of the session. Subsequent work focused on removing speech bubbles during the actual singing segment and refining the title card and audio sync. The summary notes also recorded this change as "the loading wait that timed out with the 109MB clip (networkidle -> domcontentloaded)"
.
Bonus — the same thing happens in other Playwright/Puppeteer tasks
This problem is not specific to a particular short-form video engine. When automating headless screenshots, PDF generation, E2E tests, or screen recording with Playwright or Puppeteer, networkidle-style conditions can remain unmet for a long time if the target page keeps downloading large videos or images, or has continuous network activity such as analytics beacons, WebSockets, or long polling. For these pages, measuring "loading complete" with domcontentloaded or a readiness signal sent directly by the page is safer than relying on network idle.
Reporter's Notes — why that 30-second silence never ended
page.goto. It died at the step that opens the rendering page.page.goto was waiting for.waitUntil option? Was it networkidle, by any chance?networkidle. What exactly does that wait for?networkidle from finishing its wait for the full download within 30 seconds.domcontentloaded to load the page structure first, then poll the page's __ready flag. That's the change we actually made.canplay. Without that, wait_for_function never finishes either.