grafana/k6 · error

persisting screenshot

Error message

persisting screenshot: %w

What it means

Thrown when the screenshot was captured successfully but writing it to disk via the ScreenshotPersister failed. The persister writes bytes to the path option, so this is an ordinary file-system error: unwritable directory, permission denied, disk full, or a path that names a directory.

Solutions

  1. Create the target directory and verify write permission before the run: mkdir -p shots && touch shots/.probe.
  2. Use a relative path under the current working directory, which k6 can write to.
  3. Check the wrapped error — it is the OS error (permission denied, no such file or directory, no space left on device).
  4. Free disk space or pick another volume if 'no space left on device'.

Example fix

# before
k6 run script.js  # script writes /var/out/shot.png (no dir, no perms)

# after
mkdir -p ./shots && chmod u+w ./shots
# in script: page.screenshot({ path: './shots/home.png' })
Defensive patterns

Strategy: validation

Validate before calling

import { mkdirSync, accessSync, constants } from 'fs';
mkdirSync(dir, { recursive: true });
accessSync(dir, constants.W_OK); // throws early if not writable
await page.screenshot({ path: `${dir}/shot.png` });

Prevention

When it happens

Trigger: page.screenshot({ path: '/root/shot.png' }) as a non-root user; path pointing into a nonexistent directory; read-only filesystem in a container; disk exhausted; path names an existing directory.

Common situations: CI containers running as an unprivileged user writing to paths like /app/out that were never created; k6 cloud/agent environments where the local disk is read-only; missing mkdir -p in pipelines.

Related errors


AI-assisted analysis of grafana/k6@3fcf5388d7 (2026-08-18). Data as JSON: /api/errors/336c7fb92651db4f. Report an issue: GitHub.

Appendix: source

Thrown at internal/js/modules/k6/browser/common/screenshotter.go:249

		Y:      doc.Y,
		Width:  doc.Width,
		Height: doc.Height,
		Scale:  scale,
	}
	if clip.Width > 0 && clip.Height > 0 {
		capture = capture.WithClip(clip)
	}

	// Capture screenshot
	buf, err = capture.Do(cdp.WithExecutor(s.ctx, sess))
	if err != nil {
		return nil, fmt.Errorf("capturing screenshot: %w", err)
	}

	// Save screenshot capture to file
	if path != "" {
		if err := s.persister.Persist(s.ctx, path, bytes.NewBuffer(buf)); err != nil {
			return nil, fmt.Errorf("persisting screenshot: %w", err)
		}
	}

	return buf, nil
}

func getViewPortDimensions(ctx context.Context, sess session, logger *log.Logger) (float64, float64, float64, error) {
	visualViewportScale := 1.0
	visualViewportPageX, visualViewportPageY := 0.0, 0.0

	// Add clip region
	//nolint:dogsled
	_, _, _, _, cssVisualViewport, _, err := cdppage.GetLayoutMetrics().Do(cdp.WithExecutor(ctx, sess))
	if err != nil {
		return 0, 0, 0, fmt.Errorf("getting layout metrics for screenshot: %w", err)
	}

	// we had a null pointer panic cases, when visualViewport is nil

View on GitHub (pinned to 3fcf5388d7)