[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #12 · 2026-10-01 · Example · Medium · for [ai agents](https://cdpfleet.com/ai-agents), [testing](https://cdpfleet.com/cross-browser-testing)

# Files across the wire: uploading to and downloading from a remote browser

The browser runs on our servers, your files live on your machine. setInputFiles and download.saveAs move them both ways — byte for byte.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

Run on production on 2026-10-01: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

Automations often upload a document (a CSV import, a profile photo) or download one (an invoice, an export). With a remote browser, the upload has to come from your disk and the download has to end up on your disk — not on the server running the browser. Does that work transparently, and are the files intact?

## What we used, and why

| What | Why |
| --- | --- |
| `page.setInputFiles(selector, path)` | Reads the file on your machine and streams it to the remote browser's file input. |
| `page.waitForEvent('download')` | Catches the download the click starts; the file is first saved on the browser's server. |
| `download.saveAs(path)` | Streams the downloaded file from the server to your machine. |
| `page.route` on httpbin.org | Serves a tiny page with an upload form and a download link on a real origin; httpbin echoes uploads back. |
| `/bytes/102400?seed=42` | httpbin's seeded random bytes: the same seed always gives the same 100 KB, so the download can be checked against a direct copy. |

## How it works

1.  Write a ~60 KB CSV locally and hash it.
2.  Set it into the page's file input and submit; compare the hash of what the server received.
3.  Click the download link, save the file locally and compare its hash with the same bytes fetched directly.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/12-files-up-and-down), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
import { createHash, randomBytes } from 'node:crypto';
import { writeFileSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';

const KEY = process.env.CDPFLEET_API_KEY;
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');

// A local file to upload: 2,000 CSV rows (~60 KB).
const uploadPath = path.join(tmpdir(), 'cdpfleet-upload.csv');
const rows = ['id,token'];
for (let i = 1; i <= 2000; i++) rows.push(`${i},${randomBytes(12).toString('hex')}`);
writeFileSync(uploadPath, `${rows.join('\n')}\n`);
const local = readFileSync(uploadPath);

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true }),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

// A small page on httpbin.org's origin with an upload form and a download link.
const PAGE = `<form method="post" action="/anything" enctype="multipart/form-data">
  <input type="file" name="upload" id="file"><button id="send">Send</button></form>
<a id="data" href="/bytes/102400?seed=42" download="data.bin">data</a>`;

try {
  const page = await browser.newPage();
  await page.route('https://httpbin.org/files-demo', (route) => route.fulfill({ contentType: 'text/html', body: PAGE }));
  await page.goto('https://httpbin.org/files-demo', { timeout: 60000 });

  // Upload: setInputFiles reads the file HERE and streams it to the remote browser.
  let t = Date.now();
  await page.setInputFiles('#file', uploadPath);
  const [answer] = await Promise.all([page.waitForNavigation({ timeout: 60000 }), page.click('#send')]);
  const echoed = (await answer.json()).files.upload;
  const uploadMs = Date.now() - t;

  // Download: the file lands on the remote server; saveAs streams it back to this machine.
  await page.goto('https://httpbin.org/files-demo', { timeout: 60000 });
  t = Date.now();
  const [download] = await Promise.all([page.waitForEvent('download', { timeout: 60000 }), page.click('#data')]);
  const downloadPath = path.join(tmpdir(), download.suggestedFilename());
  await download.saveAs(downloadPath);
  const downloadMs = Date.now() - t;
  const got = readFileSync(downloadPath);
  // The same seeded bytes fetched directly from here, to prove the copy is exact.
  const direct = Buffer.from(await (await fetch('https://httpbin.org/bytes/102400?seed=42')).arrayBuffer());

  console.log(JSON.stringify({
    upload: {
      local_file: path.basename(uploadPath), bytes: local.length, sha256: sha256(local),
      server_received_bytes: Buffer.byteLength(echoed), server_sha256: sha256(Buffer.from(echoed)),
      identical: sha256(local) === sha256(Buffer.from(echoed)), ms: uploadMs,
    },
    download: {
      suggested_filename: download.suggestedFilename(), bytes: got.length, sha256: sha256(got),
      direct_sha256: sha256(direct), identical: sha256(got) === sha256(direct), ms: downloadMs,
    },
  }, null, 2));
} finally {
  await browser.close();
}
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import hashlib
import json
import os
import secrets
import tempfile
import time

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]
sha256 = lambda b: hashlib.sha256(b).hexdigest()

# A local file to upload: 2,000 CSV rows (~60 KB).
upload_path = os.path.join(tempfile.gettempdir(), "cdpfleet-upload.csv")
with open(upload_path, "w") as f:
    f.write("id,token\n" + "".join(f"{i},{secrets.token_hex(12)}\n" for i in range(1, 2001)))
local = open(upload_path, "rb").read()

res = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY},
                    json={"proxy": os.environ["PROXY_URL"], "headless": True}, timeout=60)
res.raise_for_status()

# A small page on httpbin.org's origin with an upload form and a download link.
PAGE = """<form method="post" action="/anything" enctype="multipart/form-data">
  <input type="file" name="upload" id="file"><button id="send">Send</button></form>
<a id="data" href="/bytes/102400?seed=42" download="data.bin">data</a>"""

with sync_playwright() as p:
    browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
    try:
        page = browser.new_page()
        page.route("https://httpbin.org/files-demo", lambda route: route.fulfill(content_type="text/html", body=PAGE))
        page.goto("https://httpbin.org/files-demo", timeout=60000)

        # Upload: set_input_files reads the file HERE and streams it to the remote browser.
        t = time.time()
        page.set_input_files("#file", upload_path)
        with page.expect_navigation(timeout=60000) as nav:
            page.click("#send")
        echoed = nav.value.json()["files"]["upload"].encode()
        upload_ms = round((time.time() - t) * 1000)

        # Download: the file lands on the remote server; save_as streams it back here.
        page.goto("https://httpbin.org/files-demo", timeout=60000)
        t = time.time()
        with page.expect_download(timeout=60000) as dl:
            page.click("#data")
        download_path = os.path.join(tempfile.gettempdir(), dl.value.suggested_filename)
        dl.value.save_as(download_path)
        download_ms = round((time.time() - t) * 1000)
        got = open(download_path, "rb").read()
        # The same seeded bytes fetched directly from here, to prove the copy is exact.
        direct = requests.get("https://httpbin.org/bytes/102400?seed=42", timeout=60).content

        print(json.dumps({
            "upload": {"local_file": os.path.basename(upload_path), "bytes": len(local), "sha256": sha256(local),
                       "server_received_bytes": len(echoed), "server_sha256": sha256(echoed),
                       "identical": sha256(local) == sha256(echoed), "ms": upload_ms},
            "download": {"suggested_filename": dl.value.suggested_filename, "bytes": len(got), "sha256": sha256(got),
                         "direct_sha256": sha256(direct), "identical": sha256(got) == sha256(direct), "ms": download_ms},
        }, indent=2))
    finally:
        browser.close()
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.*;
import java.util.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final Path TMP = Paths.get(System.getProperty("java.io.tmpdir"));

  static String sha256(byte[] b) throws Exception {
    return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(b));
  }

  // A small page on httpbin.org's origin with an upload form and a download link.
  static final String PAGE = """
      <form method="post" action="/anything" enctype="multipart/form-data">
        <input type="file" name="upload" id="file"><button id="send">Send</button></form>
      <a id="data" href="/bytes/102400?seed=42" download="data.bin">data</a>""";

  public static void main(String[] args) throws Exception {
    // A local file to upload: 2,000 CSV rows (~60 KB).
    Path uploadPath = TMP.resolve("cdpfleet-upload.csv");
    StringBuilder csv = new StringBuilder("id,token\n");
    SecureRandom rnd = new SecureRandom();
    for (int i = 1; i <= 2000; i++) {
      byte[] token = new byte[12];
      rnd.nextBytes(token);
      csv.append(i).append(',').append(HexFormat.of().formatHex(token)).append('\n');
    }
    Files.writeString(uploadPath, csv);
    byte[] local = Files.readAllBytes(uploadPath);

    HttpClient http = HttpClient.newHttpClient();
    String body = "{\"proxy\": " + new Gson().toJson(System.getenv("PROXY_URL")) + ", \"headless\": true}";
    HttpResponse<String> res = http.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();

    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        Page page = browser.newPage();
        page.route("https://httpbin.org/files-demo", route -> route.fulfill(new Route.FulfillOptions().setContentType("text/html").setBody(PAGE)));
        page.navigate("https://httpbin.org/files-demo", new Page.NavigateOptions().setTimeout(60000));

        // Upload: setInputFiles reads the file HERE and streams it to the remote browser.
        long t = System.currentTimeMillis();
        page.setInputFiles("#file", uploadPath);
        Response answer = page.waitForNavigation(new Page.WaitForNavigationOptions().setTimeout(60000), () -> page.click("#send"));
        byte[] echoed = JsonParser.parseString(answer.text()).getAsJsonObject().getAsJsonObject("files").get("upload").getAsString()
            .getBytes(StandardCharsets.UTF_8);
        long uploadMs = System.currentTimeMillis() - t;

        // Download: the file lands on the remote server; saveAs streams it back here.
        page.navigate("https://httpbin.org/files-demo", new Page.NavigateOptions().setTimeout(60000));
        t = System.currentTimeMillis();
        Download download = page.waitForDownload(new Page.WaitForDownloadOptions().setTimeout(60000), () -> page.click("#data"));
        Path downloadPath = TMP.resolve(download.suggestedFilename());
        download.saveAs(downloadPath);
        long downloadMs = System.currentTimeMillis() - t;
        byte[] got = Files.readAllBytes(downloadPath);
        // The same seeded bytes fetched directly from here, to prove the copy is exact.
        byte[] direct = http.send(HttpRequest.newBuilder(URI.create("https://httpbin.org/bytes/102400?seed=42")).build(),
            HttpResponse.BodyHandlers.ofByteArray()).body();

        JsonObject up = new JsonObject();
        up.addProperty("local_file", uploadPath.getFileName().toString());
        up.addProperty("bytes", local.length);
        up.addProperty("sha256", sha256(local));
        up.addProperty("server_received_bytes", echoed.length);
        up.addProperty("server_sha256", sha256(echoed));
        up.addProperty("identical", sha256(local).equals(sha256(echoed)));
        up.addProperty("ms", uploadMs);
        JsonObject down = new JsonObject();
        down.addProperty("suggested_filename", download.suggestedFilename());
        down.addProperty("bytes", got.length);
        down.addProperty("sha256", sha256(got));
        down.addProperty("direct_sha256", sha256(direct));
        down.addProperty("identical", sha256(got).equals(sha256(direct)));
        down.addProperty("ms", downloadMs);
        JsonObject out = new JsonObject();
        out.add("upload", up);
        out.add("download", down);
        System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
      } finally {
        browser.close();
      }
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Diagnostics;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
string Sha256(byte[] b) => Convert.ToHexString(SHA256.HashData(b)).ToLowerInvariant();

// A local file to upload: 2,000 CSV rows (~60 KB).
var uploadPath = Path.Combine(Path.GetTempPath(), "cdpfleet-upload.csv");
var csv = new StringBuilder("id,token\n");
for (var i = 1; i <= 2000; i++) csv.Append($"{i},{Convert.ToHexString(RandomNumberGenerator.GetBytes(12)).ToLowerInvariant()}\n");
await File.WriteAllTextAsync(uploadPath, csv.ToString());
var local = await File.ReadAllBytesAsync(uploadPath);

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
    new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = true });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;

// A small page on httpbin.org's origin with an upload form and a download link.
const string PageHtml = """
    <form method="post" action="/anything" enctype="multipart/form-data">
      <input type="file" name="upload" id="file"><button id="send">Send</button></form>
    <a id="data" href="/bytes/102400?seed=42" download="data.bin">data</a>
    """;

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
    var page = await browser.NewPageAsync();
    await page.RouteAsync("https://httpbin.org/files-demo", route => route.FulfillAsync(new() { ContentType = "text/html", Body = PageHtml }));
    await page.GotoAsync("https://httpbin.org/files-demo", new() { Timeout = 60000 });

    // Upload: SetInputFilesAsync reads the file HERE and streams it to the remote browser.
    var sw = Stopwatch.StartNew();
    await page.SetInputFilesAsync("#file", uploadPath);
    var answer = await page.RunAndWaitForNavigationAsync(() => page.ClickAsync("#send"), new() { Timeout = 60000 });
    var echoed = Encoding.UTF8.GetBytes((string)JsonNode.Parse(await answer!.TextAsync())!["files"]!["upload"]!);
    var uploadMs = sw.ElapsedMilliseconds;

    // Download: the file lands on the remote server; SaveAsAsync streams it back here.
    await page.GotoAsync("https://httpbin.org/files-demo", new() { Timeout = 60000 });
    sw.Restart();
    var download = await page.RunAndWaitForDownloadAsync(() => page.ClickAsync("#data"), new() { Timeout = 60000 });
    var downloadPath = Path.Combine(Path.GetTempPath(), download.SuggestedFilename);
    await download.SaveAsAsync(downloadPath);
    var downloadMs = sw.ElapsedMilliseconds;
    var got = await File.ReadAllBytesAsync(downloadPath);
    // The same seeded bytes fetched directly from here, to prove the copy is exact.
    var direct = await http.GetByteArrayAsync("https://httpbin.org/bytes/102400?seed=42");

    var result = new JsonObject
    {
        ["upload"] = new JsonObject
        {
            ["local_file"] = Path.GetFileName(uploadPath), ["bytes"] = local.Length, ["sha256"] = Sha256(local),
            ["server_received_bytes"] = echoed.Length, ["server_sha256"] = Sha256(echoed),
            ["identical"] = Sha256(local) == Sha256(echoed), ["ms"] = uploadMs,
        },
        ["download"] = new JsonObject
        {
            ["suggested_filename"] = download.SuggestedFilename, ["bytes"] = got.Length, ["sha256"] = Sha256(got),
            ["direct_sha256"] = Sha256(direct), ["identical"] = Sha256(got) == Sha256(direct), ["ms"] = downloadMs,
        },
    };
    Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
    await browser.CloseAsync();
}
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main

import (
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

func launch(name string, options map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(options)
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

func must[T any](v T, err error) T {
	if err != nil {
		log.Fatal(err)
	}
	return v
}

func sum(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }

// A small page on httpbin.org's origin with an upload form and a download link.
const pageHTML = `<form method="post" action="/anything" enctype="multipart/form-data">
  <input type="file" name="upload" id="file"><button id="send">Send</button></form>
<a id="data" href="/bytes/102400?seed=42" download="data.bin">data</a>`

func main() {
	// A local file to upload: 2,000 CSV rows (~60 KB).
	uploadPath := filepath.Join(os.TempDir(), "cdpfleet-upload.csv")
	var csv strings.Builder
	csv.WriteString("id,token\n")
	for i := 1; i <= 2000; i++ {
		token := make([]byte, 12)
		rand.Read(token)
		fmt.Fprintf(&csv, "%d,%s\n", i, hex.EncodeToString(token))
	}
	must(0, os.WriteFile(uploadPath, []byte(csv.String()), 0o600))
	local := must(os.ReadFile(uploadPath))

	session := must(launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": true}))
	pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
	defer pw.Stop()
	browser := must(pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}}))
	defer browser.Close()

	page := must(browser.NewPage())
	must(0, page.Route("https://httpbin.org/files-demo", func(route playwright.Route) {
		route.Fulfill(playwright.RouteFulfillOptions{ContentType: playwright.String("text/html"), Body: pageHTML})
	}))
	goTo := func() {
		must(page.Goto("https://httpbin.org/files-demo", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}))
	}
	goTo()

	// Upload: SetInputFiles reads the file HERE and streams it to the remote browser.
	t := time.Now()
	must(0, page.SetInputFiles("#file", uploadPath))
	answer := must(page.ExpectNavigation(func() error { return page.Click("#send") }, playwright.PageExpectNavigationOptions{Timeout: playwright.Float(60000)}))
	var echo struct {
		Files struct{ Upload string } `json:"files"`
	}
	must(0, answer.JSON(&echo))
	echoed := []byte(echo.Files.Upload)
	uploadMs := time.Since(t).Milliseconds()

	// Download: the file lands on the remote server; SaveAs streams it back here.
	goTo()
	t = time.Now()
	download := must(page.ExpectDownload(func() error { return page.Click("#data") }, playwright.PageExpectDownloadOptions{Timeout: playwright.Float(60000)}))
	downloadPath := filepath.Join(os.TempDir(), download.SuggestedFilename())
	must(0, download.SaveAs(downloadPath))
	downloadMs := time.Since(t).Milliseconds()
	got := must(os.ReadFile(downloadPath))
	// The same seeded bytes fetched directly from here, to prove the copy is exact.
	res := must(http.Get("https://httpbin.org/bytes/102400?seed=42"))
	direct := must(io.ReadAll(res.Body))
	res.Body.Close()

	out, _ := json.MarshalIndent(map[string]any{
		"upload": map[string]any{"local_file": filepath.Base(uploadPath), "bytes": len(local), "sha256": sum(local),
			"server_received_bytes": len(echoed), "server_sha256": sum(echoed), "identical": sum(local) == sum(echoed), "ms": uploadMs},
		"download": map[string]any{"suggested_filename": download.SuggestedFilename(), "bytes": len(got), "sha256": sum(got),
			"direct_sha256": sum(direct), "identical": sum(got) == sum(direct), "ms": downloadMs},
	}, "", "  ")
	fmt.Println(string(out))
}
```

## What we got

| Direction | Bytes | Identical | Time (ms) |
| --- | --- | --- | --- |
| upload cdpfleet-upload.csv | 58902 | yes | 3134 |
| download data.bin | 102400 | yes | 1286 |

From the Node.js run on 2026-10-01. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
{
  "upload": {
    "local_file": "cdpfleet-upload.csv",
    "bytes": 58902,
    "sha256": "f86a1140019393e500309dc68217d77ea6410128cc3dc8630629f423dacbaadd",
    "server_received_bytes": 58902,
    "server_sha256": "f86a1140019393e500309dc68217d77ea6410128cc3dc8630629f423dacbaadd",
    "identical": true,
    "ms": 3134
  },
  "download": {
    "suggested_filename": "data.bin",
    "bytes": 102400,
    "sha256": "3281a765f460e525539f06ff6f9811b0c5d3a081d30d50a50d90a10d6cfa78d1",
    "direct_sha256": "3281a765f460e525539f06ff6f9811b0c5d3a081d30d50a50d90a10d6cfa78d1",
    "identical": true,
    "ms": 1286
  }
}
```

## Takeaways

-   **Both directions are byte-exact:** the uploaded CSV and the downloaded file hash identically on both ends.
-   **No extra work for "remote":** the same Playwright calls you'd use locally; the client streams the files over the session's WebSocket.
-   **Files never reach your machine on their own:** a download stays on the server until you call `saveAs` (or read `createReadStream`) — and is deleted with the session.
-   Big files cost transfer time over the WebSocket and proxy bandwidth for what the page itself downloads; check both before moving gigabytes.

---
Source: https://cdpfleet.com/docs/cases/files-up-and-down · Markdown: https://cdpfleet.com/docs/cases/files-up-and-down.md · All docs for AI tools: https://cdpfleet.com/llms.txt
