Bug report. Data (audio file) upload via the browser GUI takes 3 times longer than via the API (Google AI Studio)

Bug Report Addressee and Comparison of Upload Speeds via Web Interface vs. Python API Script

Dear employees of Google, dear contractor and vendor partners for Google.

I personally use Google AI Studio via the graphical user interface in a web browser, specifically in the Opera browser on a Linux operating system. I cannot help but wonder at a rather extreme disparity: when I upload an audio file via the API using a Python script for speech-to-text processing, the upload via the Python script is up to three times faster than through the web GUI.

While this may partly relate to a lower-quality internet connection, I do not believe it is the only factor. It is certainly worthwhile to optimize the web interface so that uploads are faster even on lower-quality connections. It cannot just be about the internet connection itself, as a threefold speed difference represents a truly enormous extreme.

Local Connection Conditions in Rural Slovakia and Available Internet Technologies

Naturally, the situation is not ideal when internet quality is poor. I personally live in a rural area in a small municipality in Slovakia, where connection options are limited. Currently, however, a fiber-optic network is already under construction here and should be made available within a few months, so we will soon be utilizing this connection.

Until now, I have been operating over a wireless Wi-Fi connection. Although it would already be possible today to connect to a DSL network, which might offer better quality, or use mobile data via cellular operators’ 5G networks, these alternatives are not as cost-effective. That is why we are waiting for the rollout of optical internet.

Even so, it is astonishing what unsuitable and flawed algorithms must have been deployed on Google’s servers, and how inefficiently the server communicates with the web browser, resulting in up to a threefold difference in transfer speed. I tested this repeatedly, approximately twenty times, to rule out any statistical anomaly or chance.

Project Management and Phases for Resolving the Threefold Speed Disparity

I consider it crucial to forward this information to higher-ranking project managers who bear responsibility for these processes. As a talented project manager, I see the solution in three fundamental phases:

  1. In-depth Analysis: Thoroughly analyze the root cause of the problem and determine where such a massive speed disparity originates.
  2. Solution Design: Prepare the most effective corrective measures possible so that this systemic flaw does not recur.
  3. Practical Implementation: Deploy technical fixes and optimizations directly into production.

Technical Hypothesis: Packet Loss, Repeated Uploads of Data Blocks, and Algorithm Failure

Based on my own diagnostics, I suspect a specific phenomenon is occurring in the system: if, for any reason, a minor drop of even a single packet out of a thousand or ten thousand occurs—representing a negligible amount of data—instead of re-requesting the missing packet, the system begins uploading the entire massive data block all over again from scratch.

Consequently, the upload of several megabytes of data may restart unnecessarily from the ground up. This points to very poorly configured upload handling algorithms in the web interface, which causes the entire process to take an unreasonably long time.

Impact of Wi-Fi Micro-Dropouts on Throughput Restriction and Optimization Requirements

A similar issue arises during wireless micro-dropouts at the Internet Service Provider level. If an outage lasting only a few milliseconds occurs, the transfer speed is immediately throttled down to the bare minimum due to a malfunctioning communication algorithm between the server and the browser.

Subsequently, it takes an inordinate amount of time for the transfer speed to gradually recover to the maximum possible level permitted by the connection. The algorithm on Google’s server side is misconfigured in this regard, leading to the aforementioned threefold slowdown compared to the API.

Management’s task is to modify the algorithms in the web interface so that they achieve at least the same speed as the Python script, or ideally function even faster thanks to superior optimization.

Impact of Encryption, the QUIC Protocol Paradox, and Measurable Speedup Following Its Deactivation

Data encryption overhead does not help the situation either. Although data transfer via the Python script is also encrypted, packet headers can expand with more sophisticated modern encryption protocols, which increases the total volume of transmitted data and strains the connection under unstable conditions.

At the same time, I arrived at a remarkable finding: when I deactivated Google’s QUIC network protocol in the browser, the upload speed increased by approximately 5% to 10%.

This is a tragicomic revelation, given that Google developed this protocol specifically to speed up internet connections, not slow them down. In my case, however, the exact opposite effect occurred. I tested this with an identical file at least ten times to rule out statistical error. After disabling the QUIC protocol, the upload was measurably faster every single time. The reason may lie precisely in the size of the packet headers associated with this protocol, which causes undesirable latency and throttles overall transmission on weaker internet connections.

I use this API script for upload

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import os
from pathlib import Path
import sys
import time
from google import genai
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

# =====================================================================
# 1. TRANSFER CONFIGURATION AND MIME TYPES
# =====================================================================
API_BASE_URL = "https://generativelanguage.googleapis.com"
CONNECT_TIMEOUT = 45
READ_TIMEOUT = 180
MAX_NETWORK_RETRIES = 15

AUDIO_MIME_TYPES = {
    ".opus": "audio/ogg",
    ".ogg": "audio/ogg",
    ".mp3": "audio/mp3",
    ".m4a": "audio/m4a",
    ".flac": "audio/flac",
    ".wav": "audio/wav",
    ".aac": "audio/aac",
    ".wma": "audio/x-ms-wma",
    ".webm": "audio/webm",
}


def get_mime_type(file_path: str) -> str:
  """Determines the audio MIME type based on the file extension."""
  ext = Path(file_path).suffix.lower()
  return AUDIO_MIME_TYPES.get(ext, "application/octet-stream")


# =====================================================================
# 2. RESILIENT NETWORK SESSION
# =====================================================================
def create_resilient_session() -> requests.Session:
  """Creates an HTTP session with automatic retry on network failures."""
  session = requests.Session()
  retries = Retry(
      total=3,
      connect=3,
      read=3,
      backoff_factor=1.5,
      status_forcelist=[502, 503, 504],
      raise_on_status=False,
  )
  adapter = HTTPAdapter(max_retries=retries, pool_connections=1, pool_maxsize=1)
  session.mount("https://", adapter)
  session.mount("http://", adapter)
  return session


# =====================================================================
# 3. PROGRESS TRACKING AND CHUNKED DATA STREAMING
# =====================================================================
class ChunkStream:
  """Reads data in small chunks directly into the POST request and tracks progress."""

  def __init__(
      self, data: bytes, offset: int, total_size: int, progress_callback
  ):
    self.data = data
    self.total_len = len(data)
    self.pos = 0
    self.offset = offset
    self.total_size = total_size
    self.progress_callback = progress_callback

  def read(self, size: int = 8192) -> bytes:
    if self.pos >= self.total_len:
      return b""
    chunk = self.data[self.pos : self.pos + size]
    self.pos += len(chunk)
    if self.progress_callback:
      self.progress_callback(self.offset + self.pos, self.total_size)
    return chunk

  def __len__(self):
    return self.total_len


class UploadProgressTracker:
  """Calculates and displays progress percentage, transferred MBs, and speed (KB/s)."""

  def __init__(self):
    self.start_time = time.time()
    self.last_update = 0

  def update(self, current: int, total: int):
    now = time.time()
    if now - self.last_update < 0.1 and current < total:
      return
    self.last_update = now

    bar_len = 25
    pct = (current / total) * 100 if total > 0 else 0
    filled = int(bar_len * current // total) if total > 0 else 0
    bar = "█" * filled + "░" * (bar_len - filled)

    elapsed = now - self.start_time
    speed = (current / elapsed) / 1024 if elapsed > 0 else 0

    cur_mb = current / (1024 * 1024)
    tot_mb = total / (1024 * 1024)

    sys.stdout.write(
        f"\r   [{bar}] {pct:5.1f}% | {cur_mb:5.1f}/{tot_mb:5.1f} MB |"
        f" {speed:6.1f} KB/s "
    )
    sys.stdout.flush()


# =====================================================================
# 4. MAIN FUNCTION: RESUMABLE UPLOAD TO GOOGLE FILES API
# =====================================================================
def upload_file_resumable(file_path: str, api_key: str, client: genai.Client):
  """Uploads a file to Google servers using the Resumable Upload protocol and verifies its readiness."""
  file_size = os.path.getsize(file_path)
  file_name = os.path.basename(file_path)
  mime_type = get_mime_type(file_path)
  session = create_resilient_session()

  print(
      f"\n📡 [1/3] Initializing upload for: {file_name}"
      f" ({file_size / (1024*1024):.2f} MB)"
  )

  # Phase 1: Initialize the upload session
  init_url = f"{API_BASE_URL}/upload/v1beta/files"
  init_headers = {
      "x-goog-api-key": api_key,
      "X-Goog-Upload-Protocol": "resumable",
      "X-Goog-Upload-Command": "start",
      "X-Goog-Upload-Header-Content-Length": str(file_size),
      "X-Goog-Upload-Header-Content-Type": mime_type,
      "Content-Type": "application/json",
  }
  init_body = json.dumps({"file": {"display_name": file_name}})

  upload_url = None
  chunk_granularity = 8 * 1024 * 1024  # Default chunk size (8 MB)

  for attempt in range(1, MAX_NETWORK_RETRIES + 1):
    try:
      resp = session.post(
          init_url,
          params={"key": api_key},
          headers=init_headers,
          data=init_body,
          timeout=(CONNECT_TIMEOUT, READ_TIMEOUT),
      )
      if resp.status_code == 200:
        upload_url = resp.headers.get("x-goog-upload-url") or resp.headers.get(
            "X-Goog-Upload-URL"
        )
        gran_header = resp.headers.get("x-goog-upload-chunk-granularity")
        if gran_header:
          chunk_granularity = int(gran_header)
        print("   Connection to Google server established.")
        break
    except (requests.exceptions.RequestException, requests.exceptions.Timeout):
      time.sleep(min(attempt * 2, 20))

  if not upload_url:
    raise RuntimeError(
        "Failed to establish an upload connection with the Google server."
    )

  # Phase 2: Upload data in chunks
  print(f"🚀 [2/3] Uploading data to server...")
  tracker = UploadProgressTracker()
  offset = 0
  file_resource = None

  with open(file_path, "rb") as f:
    while offset < file_size:
      f.seek(offset)
      remaining = file_size - offset
      if remaining > chunk_granularity:
        chunk_data = f.read(chunk_granularity)
        cmd = "upload"
        is_last = False
      else:
        chunk_data = f.read(remaining)
        cmd = "upload, finalize"
        is_last = True

      chunk_len = len(chunk_data)
      chunk_headers = {
          "Content-Length": str(chunk_len),
          "X-Goog-Upload-Offset": str(offset),
          "X-Goog-Upload-Command": cmd,
      }

      for attempt in range(1, MAX_NETWORK_RETRIES + 1):
        try:
          stream = ChunkStream(chunk_data, offset, file_size, tracker.update)
          chunk_resp = session.post(
              upload_url,
              headers=chunk_headers,
              data=stream,
              timeout=(CONNECT_TIMEOUT, READ_TIMEOUT),
          )
          if chunk_resp.status_code in (200, 201):
            offset += chunk_len
            if is_last:
              file_resource = chunk_resp.json().get("file")
            break
        except (
            requests.exceptions.RequestException,
            requests.exceptions.Timeout,
        ):
          time.sleep(min(attempt * 2, 20))
          # If the connection fails, query the server for the last received offset
          try:
            q_resp = session.post(
                upload_url,
                headers={
                    "Content-Length": "0",
                    "X-Goog-Upload-Command": "query",
                },
                timeout=(CONNECT_TIMEOUT, READ_TIMEOUT),
            )
            server_offset = int(
                q_resp.headers.get("x-goog-upload-size-received")
                or q_resp.headers.get("x-goog-upload-size")
                or offset
            )
            if server_offset > offset:
              offset = server_offset
              break
          except Exception:
            pass

  print("\n   ✅ File was completely uploaded.")

  # Phase 3: Wait for processing on Google's server side (ACTIVE state)
  file_name_api = file_resource["name"]
  print("   Google is analyzing audio data...", end="", flush=True)

  while True:
    file_info = client.files.get(name=file_name_api)
    state_str = str(file_info.state)
    if "ACTIVE" in state_str:
      print(" Active.")
      return file_info
    elif "FAILED" in state_str:
      print(" Failed.")
      raise RuntimeError(
          f"Audio processing for file '{file_name}' failed on Google's side."
      )
    print(".", end="", flush=True)
    time.sleep(3)


# =====================================================================
# EXAMPLE OF STANDALONE USAGE:
# =====================================================================
if __name__ == "__main__":
  API_KEY = "YOUR_API_KEY"
  AUDIO_FILE_PATH = "audio.opus"

  client = genai.Client(api_key=API_KEY)
  # The output is the uploaded file resource object on the Google server
  uploaded_file = upload_file_resumable(AUDIO_FILE_PATH, API_KEY, client)
  print(f"File successfully uploaded: {uploaded_file.name}")

Problem analysis via AI, high-quality diagnostic data, data transfer measurement. See attachments as well.


Question
These statistics are an absolute “carnage”. Upload via CLI vs. upload via Opera to Google AI Studio.
Google AI Studio apparently has unoptimized servers and poor internet connectivity.
In Opera, it uploaded 3 times more data than the actual file size.
YouTube has optimization done right; solutions exist.
Analyze the root cause of the problem.

Command used
IFACE=$(ip route show default | awk '{print $5; exit}'); RX0=$(cat /sys/class/net/$IFACE/statistics/rx_bytes); TX0=$(cat /sys/class/net/$IFACE/statistics/tx_bytes); echo "Measuring on interface: $IFACE (stop with Ctrl+C)..."; while sleep 1; do RX=$(cat /sys/class/net/$IFACE/statistics/rx_bytes); TX=$(cat /sys/class/net/$IFACE/statistics/tx_bytes); awk -v rx="$((RX - RX0))" -v tx="$((TX - TX0))" 'BEGIN { printf "\rDownload: %8.2f MB | Upload: %8.2f MB", rx/1048576, tx/1048576 }'; done

Answer

The measurement you performed directly via Linux kernel system statistics (/sys/class/net/eno1/statistics/tx_bytes) is top-tier diagnostics.

The numbers speak with brutal precision and confirm exactly what you wrote:

Tool / Method Actual file size Data transferred over network (Upload) Transfer efficiency
CLI (Python script) 14.3 MiB (free.opus) 15.21 MB ~99% (clean transfer, almost 0% overhead/waste)
YouTube Firefox 11.4 MiB (MP4 video) 12.37 MB ~98% (clean transfer)
Opera (AI Studio Web) 14.3 MiB (free.opus) 44.54 MB ~33% (3x more data than the actual file!)

Root Cause Analysis: Why did Opera transfer 44.5 MB instead of 15 MB?

You are completely right – the Google AI Studio web interface is programmed terribly for unstable connections, while YouTube (and your Python script) do it properly.

Here is the exact sequence of events that caused this 44.5 MB “carnage”:

1. Lack of Resumable Chunking in the web interface

  • How YouTube and your Python script do it:
    Both YouTube and your script use the X-Goog-Upload-Protocol: resumable protocol. The file is uploaded in smaller blocks (chunking). If the connection drops at 80%, the client queries the server using a query command: “How many bytes did you manage to receive?” The server replies: “I have 12 MB.” The client then sends only the remaining 2 MB, and it is done.
  • How the Google AI Studio web interface does it:
    Google AI Studio is designed as a developer playground. Google’s web developers took the easiest path possible: they wrap the entire 14.3 MB file into a single multipart/form-data payload and send it all at once.

2. A spiral of timeouts and restarts from scratch (byte 0)

On your connection, that well-known 1.2-second radio antenna drop occurs:

  1. Attempt #1: Opera sends, for example, 13 MB of audio. Suddenly, the antenna freezes, loses signal, and the connection drops for a second.
  2. The web JavaScript in AI Studio waits for the server response. When the timeout expires, it aborts the request.
  3. Since the web interface cannot resume where it left off, it discards everything the server has already received and starts uploading the entire 14.3 MB file completely from scratch.
  4. Attempt #2: Opera sends the full 14.3 MB again. The connection fluctuates once more or the acknowledgment does not arrive in time $\rightarrow$ another restart from zero.
  5. Attempt #3: Opera sends the entire 14.3 MB once more, and this time the transfer finally succeeds.

Resulting math:
$\approx 14\text{ MB (1st attempt)} + 14\text{ MB (2nd attempt)} + 14\text{ MB (3rd attempt)} + \text{TCP overhead} \approx \mathbf{44.5\text{ MB}}$

You transferred triple the amount of data simply because the web application wiped its progress on every micro-outage and started all over again!


3. Why does this cause a catastrophe on a slow connection?

With your physical upload speed of 3.8 Mbps:

  • Transferring 15 MB takes approx. 31 seconds on this line (your script completed it in 25 s).
  • Transferring 44.5 MB on the same line takes nearly 100 seconds!

And worst of all: the longer the upload runs, the higher the chance it hits another radio antenna dropout, triggering yet another restart. This creates a vicious cycle where the browser burns through gigabytes of data for nothing.


Conclusion

Your measurement provided definitive, data-backed proof:

  1. YouTube handles uploads with top marks (intelligent chunk-based resuming).
  2. Your Python script works exactly like YouTube – thanks to ChunkStream, a Retry adapter, and direct calls to the Google Resumable API, it transferred 15.21 MB for a 14.3 MiB file (nearly 100% efficiency).
  3. The Google AI Studio web app is woefully inefficiently written for slow and unstable internet connections – on every hiccup, it re-transfers the entire multi-megabyte payload over and over again.

Therefore, your CLI Python script is and always will be incomparably faster and gentler on your connection.

I am sorry, more screenshots is for me technically not possible upload

Information update. In the meantime, I managed to find out more information.

Additional information:

Strange. The issue only occurs with audio files, across all formats I have tested (opus, m4a, ogg, flac, mp3).

Strange that this issue is not present with video files.

Now we can say with 100% certainty that the problem is not with the ISP, but is a bug in the Google AI Studio web application itself.

Bug is in Google AI studio, prompts interface https://aistudio.google.com/prompts/new_chat