--- title: "Browser Sequential Download Blocking — window.open Needs 400ms Gap" aliases: [browser-download-blocking, sequential-download, window-open-popup-blocker] tags: [browser, javascript, frontend, download, popup-blocker, gotcha] sources: - "daily/2026-04-30.md" created: 2026-04-30 updated: 2026-04-30 --- # Browser Sequential Download Blocking — `window.open` Needs 400ms Gap Browsers suppress sequential `window.open()` calls that happen too close together, treating them as a popup storm. When a "Download All" feature triggers multiple file downloads in a loop, only the first download succeeds — the rest are silently blocked by the browser's popup blocker. Adding a 400ms delay between each call resolves this. ## Key Points - **Browsers allow only one `window.open()` per user gesture** — subsequent calls in the same synchronous or near-synchronous execution are treated as unsolicited popups and blocked - **Only the first download succeeds** — there's no error thrown; the remaining files silently disappear - **The fix:** add a 400ms (or longer) async gap between each download trigger — this allows the browser to process each "gesture" separately - **`fetch` + `URL.createObjectURL` is the safer alternative** — avoids the popup blocker entirely by keeping the download in the same window context - The exact threshold varies by browser (Chrome ~300ms, Firefox ~400ms, Safari ~500ms) — use 500ms for cross-browser safety ## Details ### The Problem ```typescript // ❌ BROKEN — only first download succeeds async function downloadAll(fileUrls: string[]) { for (const url of fileUrls) { window.open(url, "_blank"); // ← 2nd, 3rd, etc. are silently blocked } } ``` ### Fix 1: Async Gap Between Downloads ```typescript // ✅ Add delay between each window.open call async function downloadAll(fileUrls: string[]) { for (const url of fileUrls) { window.open(url, "_blank"); await new Promise(resolve => setTimeout(resolve, 500)); // 500ms gap } } ``` ### Fix 2: Fetch + Blob URL (Popup-Blocker-Immune) ```typescript // ✅ More reliable — no popup blocker involvement async function downloadFile(url: string, filename: string) { const response = await fetch(url); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = blobUrl; anchor.download = filename; anchor.click(); URL.revokeObjectURL(blobUrl); // cleanup } async function downloadAll(files: { url: string; name: string }[]) { for (const file of files) { await downloadFile(file.url, file.name); await new Promise(resolve => setTimeout(resolve, 200)); // shorter gap needed } } ``` The `` approach doesn't trigger the popup blocker because it's a same-page navigation, not a new window. ### When `window.open` Is Required For files hosted on a different origin (where `fetch` would be blocked by CORS), or for PDFs that need to open in a new tab, `window.open` may be unavoidable. In that case, the 500ms gap is the only option. ### Diagnosing the Issue In Chrome DevTools: - A blocked popup shows a small icon in the address bar ("Popup blocked") - The browser console logs: `[blocked] Opening a URL that was denied by the popup policy` - Or: `The window was not opened — popup blocker is active` If only the first of N downloads completes, popup blocking is almost certainly the cause. ## Related Concepts - [[wiki/concepts/native-track-blob-url]] — related browser URL object pattern for VTT tracks - [[wiki/tech-patterns/react-vite-typescript]] — React frontend patterns ## Sources - [[daily/2026-04-30.md]] — video-accessibility Download All button: sequential `window.open` calls triggered popup blocker; only first file downloaded; fix was 400ms async gap between calls