Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

In headless chromedp, the return from chromedp.Run after a click does not mean the downloaded file is ready. Configure Chrome’s download behavior, enable download events, register a chromedp.ListenTarget listener before triggering the download, then wait for browser.EventDownloadProgress to report Completed. Use the event’s GUID to find the file and verify it on disk.

Why chromedp can return before a download finishes

A browser action and a file transfer are separate events. A click may successfully start a download and return while Chrome is still receiving or writing the file. Treating the return from chromedp.Run as proof that the file is complete creates a race: the next step may try to read a partial or nonexistent file.

Chrome reports download progress through the Browser domain. The generated cdproto API exposes behavior settings and progress states; the canonical chromedp project test configures downloads, listens for a completion event, clicks a link, and checks the resulting file. chromedp package documentation

Use download events and wait for completion

  1. Choose a writable directory. Create or select a dedicated directory so you can tell which files belong to the job.
  2. Set Chrome’s behavior before triggering the download. Use browser.SetDownloadBehavior with allowAndName, the directory path, and events enabled.
  3. Register the listener before the click or navigation. Listen for *browser.EventDownloadProgress and react to its state.
  4. Trigger the download. A click, navigation, or another browser action may start it.
  5. Wait for a completion signal or context cancellation. Handle both completed and canceled states; apply a practical deadline.
  6. Verify the file. With allowAndName, use the event GUID as the filename, then check the path and any application-specific integrity requirements.

The order matters: enabling events and installing the listener before the action prevents missing a fast completion event. The chromedp download test

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Runnable Go example

This example assumes the download link is available at the supplied page and can be selected with #download. It uses a buffered channel so the listener callback does not block if the download finishes while the browser action is returning. Give the context a deadline appropriate for the expected file size and network conditions.

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/chromedp/chromedp"
	"github.com/chromedp/cdproto/browser"
)

func download(ctx context.Context, pageURL, downloadDir string) (string, error) {
	if err := os.MkdirAll(downloadDir, 0755); err != nil {
		return "", fmt.Errorf("create download directory: %w", err)
	}

	done := make(chan struct {
		guid string
		err  error
	}, 1)

	chromedp.ListenTarget(ctx, func(v any) {
		ev, ok := v.(*browser.EventDownloadProgress)
		if !ok {
			return
		}
		switch ev.State {
		case browser.DownloadProgressStateCompleted:
			select {
			case done <- struct {
				guid string
				err  error
			}{guid: ev.GUID}:
			default:
			}
		case browser.DownloadProgressStateCanceled:
			select {
			case done <- struct {
				guid string
				err  error
			}{err: errors.New("download canceled")}:
			default:
			}
		}
	})

	err := chromedp.Run(ctx,
		browser.SetDownloadBehavior(browser.SetDownloadBehaviorBehaviorAllowAndName).
			WithDownloadPath(downloadDir).
			WithEventsEnabled(true),
		chromedp.Navigate(pageURL),
		chromedp.Click("#download", chromedp.ByQuery),
	)
	if err != nil {
		return "", fmt.Errorf("start download: %w", err)
	}

	select {
	case result := <-done:
		if result.err != nil {
			return "", result.err
		}
		path := filepath.Join(downloadDir, result.guid)
		if _, err := os.Stat(path); err != nil {
			return "", fmt.Errorf("download completed but file is unavailable at %q: %w", path, err)
		}
		return path, nil
	case <-ctx.Done():
		return "", fmt.Errorf("waiting for download: %w", ctx.Err())
	}
}

func main() {
	allocCtx, cancelAlloc := chromedp.NewContext(context.Background())
	defer cancelAlloc()

	ctx, cancel := context.WithTimeout(allocCtx, 2*time.Minute)
	defer cancel()

	path, err := download(ctx, "https://example.com/page-with-download", "./downloads")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println("Downloaded:", path)
}

Replace the example page URL and selector with the target page and its download control. The code uses allowAndName, so Chrome writes the file under its download GUID rather than the URL’s apparent basename. The example follows the official test pattern; the project test is the reference for the sequence, not a guarantee that a site’s own download flow uses the same selector or behavior. chromedp download test

Dependencies and context setup

Import both github.com/chromedp/chromedp and github.com/chromedp/cdproto/browser. The context passed to ListenTarget and chromedp.Run must be the same target context. In production, use a context deadline for the entire operation or derive a per-download deadline from a longer-lived browser context. A timeout bounds how long a worker can remain stuck if the site stalls or no event arrives.

Why the channel uses a result type

A completion event carries a GUID; a canceled event should be distinguishable from success. A result with an error preserves that distinction instead of using an empty GUID as an implicit cancellation marker. In a system that intentionally handles only one download at a time, a simpler buffered channel can carry the GUID, with a separate cancellation signal.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choosing the Chrome download behavior

The Browser-domain API supports four behavior values. The generated cdproto documentation says a download path is required with allow and allowAndName; progress events are disabled by default, so explicitly enabling them is essential for this event-based method. cdproto browser API documentation

Behavior What it means for this workflow
deny Disallow downloads.
allow Allow downloads; provide a download path.
allowAndName Allow downloads and name files using their download GUIDs; provide a download path. Useful for deterministic lookup from the completion event.
default Use Chrome’s default download behavior.

For a workflow that needs to correlate an event with a file, allowAndName avoids guessing from the source URL. Redirects, server-provided filenames, and browser naming behavior can make the original URL basename unreliable.

Handle multiple or concurrent downloads

Do not assume the next progress event belongs to the click you just issued if a page can start more than one download. Progress events include a GUID; retain the GUID of each download and associate its state with the right job. If your workflow can identify an expected GUID only after progress begins, maintain a small map keyed by GUID rather than consuming the first completion as a global success signal.

  • One download at a time: a buffered result channel and a dedicated directory are usually enough.
  • Several downloads from one page: track each GUID and completion or cancellation independently.
  • Several browser jobs at once: isolate download directories per job and use separate contexts/listeners to reduce accidental file association.

After a completion event, verify the corresponding path. If correctness is important, also validate expected size, MIME type, file signature, or checksum. The event indicates Chrome’s download completed; it does not establish that the file is the content your application expected.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Filesystem polling as a fallback

If progress events are unavailable in a particular integration, polling the directory can work, but a file’s first appearance is not proof that writing has finished. Use a timeout and require file size to remain unchanged over multiple intervals before treating it as stable. This is a fallback, not as precise as receiving Chrome’s completion state, and it still needs cancellation and partial-file handling.

Troubleshooting common failures

No progress event arrives

  • Confirm the listener was registered before the click or navigation.
  • Confirm the behavior call used WithEventsEnabled(true); the API field defaults to false.
  • Check that the event callback is listening on the target context used for the download.
  • Keep the context alive until the completion wait ends.

The click succeeds but the file is missing

  • Do not treat the click or chromedp.Run return as completion.
  • Check that the path exists and is writable by the browser process.
  • With allowAndName, inspect the GUID path rather than expecting the URL filename.
  • Verify the page actually initiated a download rather than opening content in a tab or returning an error page.

The operation hangs indefinitely

Set a context deadline and return its error when the wait expires. A timeout is a safety boundary for a stalled request or a missed event, not proof that Chrome canceled the network transfer. Decide whether the caller should retry, cancel the browser context, or inspect the download directory before retrying to avoid confusing a late file with a new attempt. chromedp issue discussing download behavior and waiting

The browser reports cancellation

Handle DownloadProgressStateCanceled explicitly and surface it as a failed download. Inspect the site response and browser logs, confirm the target remains available, and avoid reading a partial file as a successful result.

Performance, reliability, and cost considerations

  • Bound each wait: choose a deadline based on expected download size and the slowest acceptable network, rather than allowing a worker to block forever.
  • Limit concurrency deliberately: concurrent downloads need per-GUID state and directory isolation, and consume browser, disk, and network resources.
  • Clean up intentionally: remove temporary files after validation or retain them according to the application’s needs; do not reuse a shared directory without a strategy for identifying stale files.
  • Validate before consuming: completion is a transport-level signal, not a guarantee of file type, content, or business correctness.
  • Use the right mechanism: events are the direct completion signal; polling adds filesystem checks and stability delays.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If the job is to capture a page rather than download a file through Chrome, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. For example, the cURL call below saves a WebP screenshot; see the ScreenshotNeo documentation for parameters and response details.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie or consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.

Sign up for 1,000 free screenshots a month—no card required.

Frequently Asked Questions

Which event state means Chrome finished downloading?

Use browser.DownloadProgressStateCompleted from browser.EventDownloadProgress.

Does allowAndName preserve the downloaded filename?

No. It names the file with the download GUID, which you can use to locate it deterministically.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I use a different language with ScreenshotNeo?

Yes. The API accepts a GET request, and the documentation includes request options for integrating it with your application.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.