Upload speeds extremely slow / stalling since April 1st

Since yesterday afternoon (April 1st), I’ve been experiencing extremely slow upload speeds when uploading GGUF model files to the Hub using hf upload. The uploads start at a reasonable speed but progressively slow down from ~1 MB/s, then downgrades to a few KB/s, and eventually stall completely at ~110 KB/s with seemingly no progress at all.

What I’ve tried:

  • Uploading all files at once vs single file, same issue
  • Disabling xet (HF_HUB_ENABLE_XET=0) and hf-transfer (HF_HUB_ENABLE_HF_TRANSFER=0), same issue
  • Using an older version of huggingface-hub (0.36.2) — same issue
  • Checked status.huggingface.co, no reported issues
  • My internet connection is fine for everything else

The pattern is consistent: uploads begin at normal speed, then gradually degrade over a few minutes until they completely stop progressing. This happens regardless of file size or number of files being uploaded.

Environment:

  • huggingface-hub: 1.8.0 (also tested with 0.36.2)
  • OS: Windows 11 Pro
  • Files: GGUF model files ranging from 11GB to 47GB

This was working fine until yesterday. Is anyone else experiencing similar issues?

1 Like

Possibly bug of Windows?


This likely matches huggingface_hub issue #3871 and PR #3912. It does not look like a new April 1 outage. The pattern matches a Windows multipart upload bug for files with new data over ~2 GB. HF_HUB_ENABLE_HF_TRANSFER is deprecated now, so toggling it is not informative. The best workaround for now is trying the upload from WSL/Linux/macOS, and using the official HF_HUB_DISABLE_XET=1 flag for a clean non-Xet test. Uploads with `New Data Upload` over 2GB get stuck in Windows 11 with Python 3.12 · Issue #3871 · huggingface/huggingface_hub · GitHub

Possibly bug of Windows?

Very unlikely because hf upload was working no issue until April 1st.

1 Like

Very unlikely because hf upload was working no issue until April 1st.

I thought, “True.” but… it seems there were several library updates around that time… @rajatarya


Most likely, you are hitting a Windows-specific large-file upload bug or regression in the current Xet-backed upload path, not a broad Hugging Face outage. The two strongest signals are that Hugging Face’s public status page shows no reported incidents for February through April 2026, and there is an open huggingface_hub issue for Windows 11 where uploads with new data above about 2 GB slow down around 1.98 GB and then stop. A linked PR proposes changing Windows multipart uploads from seek-based reads to sequential reads, which points at a client-side Windows path problem rather than a general Hub outage. (Hugging Peace Status)

Why your case fits that explanation

Your symptom is not “instant failure.” It is “starts normally, then progressively decays, then stalls.” That pattern matches how the current upload stack works. Hugging Face’s upload docs say hf_xet scans files, chunks them, packages those chunks into local blocks, and uploads them to the Xet content-addressable service. The Xet docs also say adaptive concurrency is on by default, so the client actively changes parallelism while the transfer is running. That means a transfer can look healthy at first and then become unstable later if the Windows read path, retry logic, or concurrency controller gets into a bad state. (Hugging Face)

Why “it worked until April 1” does not rule out a Windows bug

That objection is reasonable. It does not prove there was no Windows bug. It means the bug may have become easier to trigger or more frequent around that date. The release timing is relevant here: huggingface_hub v1.8.0 shipped on March 25, 2026 and its release notes say the hf-xet bump to v1.4.2 should “greatly improve upload speed of large files”; hf-xet 1.4.3 was released on March 31, 2026; and huggingface_hub v1.9.0 shipped on April 2, 2026 with “Bump to hf-xet 1.4.3 and add regression test.” That does not prove a regression was introduced on April 1, but it does show that the transfer stack was actively changing in exactly that window. (GitHub)

Background that matters here

The old upload mental model is outdated. Hugging Face’s migration guide says all repositories are now Xet-enabled, hf_xet is the default upload and download path, and HF_HUB_ENABLE_HF_TRANSFER is ignored. The upload guide also says that as of huggingface_hub 0.32.0, installing huggingface_hub also installs hf_xet. So testing with 0.36.2 does not take you back to the old pre-Xet world. It is still in the Xet era. (Hugging Face)

That also means one of your tests was weaker than it looked. The official documented switch is HF_HUB_DISABLE_XET, not HF_HUB_ENABLE_XET=0. The docs list HF_HUB_DISABLE_XET among the relevant Xet environment variables, and a Hugging Face staff reply in another upload thread explicitly says that setting HF_HUB_DISABLE_XET=1 will fall back to HTTP upload. On top of that, the release notes say environment variables are read at import time, so setting them too late can make the test misleading. (Hugging Face)

What I think is happening, ranked

1. Most likely: Windows multipart upload bug in the Xet-backed path

This is still the best match. The open issue says Windows uploads with new data over ~2 GB get stuck and the linked PR is explicitly about Windows multipart upload reads. Your file sizes are all well above that threshold. The fact that the transfer starts fine and then degrades also fits this class of bug better than a permissions problem or a generic API failure. (GitHub)

2. Also likely: recent Xet changes made an existing Windows edge case easier to trigger

This is an inference, but it is a grounded one. You have date-localized onset around April 1. The transfer stack changed on March 25, March 31, and April 2. There is no public outage. The cleanest explanation is not “the whole Hub broke,” but “recent client-side transport changes intersected with a Windows-specific weakness.” (GitHub)

3. Plausible: cache-path or local-disk pressure is contributing

Xet does local chunking and writes local blocks before upload. The docs say HF_XET_CACHE defaults under HF_HOME, and the upload guide warns that upload performance depends on where that cache lives. There is also a forum case where HF_XET_CACHE grew to around 200 GB and filled the tmpfs, which caused upload problems. Your symptom is not the same error, but if your cache drive is slow, fragmented, or low on free space, it can amplify transport instability. (Hugging Face)

4. Less likely: commit-size or repo-structure issue

Hugging Face’s storage limits doc says that commit validation problems tend to show up when a commit contains many operations, because each operation is checked and HTTP pushes can hit a 60s client timeout. That is usually more correlated with many files per commit than with a single huge file. Since you see the same degradation even on a single large GGUF file, I would put this lower on the list. (Hugging Face)

5. Unlikely: broad Hugging Face outage

There were no reported incidents in March or April 2026 on the public status page. That does not make a narrow backend regression impossible, but it makes a broad platform outage the weakest explanation. (Hugging Peace Status)

What your past tests really tell us

Your “single file vs all files” test tells me the problem is probably not primarily about commit fanout. If it were a many-operation commit problem, single-file uploads would usually behave noticeably better. That inference lines up with the storage-limits guidance about operation count per commit. (Hugging Face)

Your “older version 0.36.2” test tells me less than it seems to. Since Xet became the default path by the 0.32.x era, that downgrade still leaves you on the Xet-backed architecture. So it does not cleanly answer “does the old non-Xet path work?” (Hugging Face)

Your “disabled xet” test also does not settle it, because the documented variable is HF_HUB_DISABLE_XET, not HF_HUB_ENABLE_XET=0, and environment variables are read at import time. So that test may not actually have exited the Xet path. (Hugging Face)

What I would do next

These are the tests that actually separate the causes.

A. Run one clean HTTP fallback test

This is the highest-value next step.

$env:HF_HUB_DISABLE_XET="1"
$env:HF_DEBUG="1"
hf env
hf upload ...

Why this matters: Hugging Face staff explicitly recommends HF_HUB_DISABLE_XET=1 as a temporary unblock in another upload thread, and says it falls back to HTTP upload. If this works, the Xet path becomes the main suspect. If it fails in the same way, then the problem is either more general to Windows or below the Xet layer. (Hugging Face Forums)

B. Do a native Windows vs WSL/Linux A/B test

Use the same file, same repo, same token, same network.

Why this matters: the closest public bug and fix proposal are both Windows-specific. So a WSL or Linux success with a native Windows failure would strongly support the Windows-path diagnosis. This is an inference from the issue and PR, but it is the cleanest one. (GitHub)

C. Move the Xet cache to a fast local disk with plenty of free space

This is especially worth doing on Windows if your user profile is on a slower drive.

$env:HF_XET_CACHE="D:\hf-xet-cache"
$env:HF_DEBUG="1"
hf env
hf upload ...

Why this matters: the docs say Xet writes upload blocks locally before sending them, HF_XET_CACHE defaults under HF_HOME, and the cache location can affect upload behavior. There is also at least one real-world report where cache growth caused upload issues. (Hugging Face)

D. Pin upload concurrency to the minimum for diagnosis

Do this only if you want to test whether the adaptive controller is the trigger.

Remove-Item Env:HF_HUB_DISABLE_XET -ErrorAction SilentlyContinue
$env:HF_XET_CLIENT_ENABLE_ADAPTIVE_CONCURRENCY="false"
$env:HF_XET_FIXED_UPLOAD_CONCURRENCY="1"
$env:HF_DEBUG="1"
hf env
hf upload ...

Why this matters: Xet uses adaptive concurrency by default, and the docs say HF_XET_FIXED_UPLOAD_CONCURRENCY bypasses that controller by pinning concurrency to a fixed value. If the upload becomes stable at fixed concurrency 1, that points to a controller or parallelism interaction rather than a basic connectivity failure. (Hugging Face)

E. Use hf upload-large-folder for a more resilient path

If you are pushing multiple GGUFs or want better crash recovery, use the large-folder command.

hf upload-large-folder <repo_id> <folder> --repo-type=model --num-workers=4

Why this matters: the official upload guide says upload_large_folder() and hf upload-large-folder are meant for large data, add a resilient retry mechanism, and are usually preferable when you care more about robustness than about a single clean commit. The same guide also warns that permanent errors will keep retrying, so this is a resilience tool, not a root-cause fix. (Hugging Face)

What I would not do first

I would not start with HF_XET_HIGH_PERFORMANCE=1. The docs say high-performance mode tries to use all available bandwidth and CPU cores. That can help on a healthy fast path, but if the path is already unstable, more concurrency can make diagnosis worse, not better. (Hugging Face)

I would also not assume that your file sizes themselves are the problem. Hugging Face’s storage guidance says single files up to 500 GB are supported, with files under 200 GB recommended for best experience. Your 11 GB to 47 GB GGUF files are large, but they are well inside the documented envelope. (Hugging Face)

My actual bottom line

My current view is:

  • Most likely root cause: a Windows-specific bug or regression in the current Xet-backed large-file upload path. (GitHub)
  • Why April 1 matters: not because there was a public outage, but because the upload stack changed right around then. (GitHub)
  • Why your current evidence is not decisive yet: the non-Xet test was not clean, and 0.36.2 is still in the Xet-default era. (Hugging Face)
  • Best next two tests: HF_HUB_DISABLE_XET=1 done correctly, then native Windows vs WSL/Linux. (Hugging Face Forums)

If those two tests come out like this:

  • HTTP fallback works, native Windows Xet fails
    then I would treat this as an almost confirmed Xet-on-Windows regression.

  • HTTP fallback fails too, but WSL/Linux works
    then I would treat it as a broader Windows upload-path problem, possibly not limited to Xet.

  • Everything fails the same way
    then I would raise the probability of a narrower Hugging Face backend regression or a repo-specific server-side interaction, even though the public status page stayed green. (Hugging Peace Status)


Use this. It is one file, no argparse, and it uses the documented Python API HfApi.upload_file() directly. It sets environment variables before importing huggingface_hub, because Hugging Face notes that those variables are read at import time. It also uses the current official Xet controls: HF_HUB_DISABLE_XET for HTTP fallback, HF_DEBUG for verbose request logging, HF_XET_CACHE for the local Xet cache, and fixed-concurrency knobs to bypass adaptive concurrency when you want a cleaner repro. HF_HUB_ENABLE_HF_TRANSFER is deprecated and not useful here. (Hugging Face)

One important caveat: for a fair comparison across modes, do one run per execution against a file whose bytes have not already been uploaded to the Hub. Xet does chunk-level deduplication, so rerunning the same file in several modes can hide the real upload path by skipping most “new data.” (Hugging Face)

# windows_hf_upload_probe.py
#
# Minimal Windows troubleshooting script for large uploads to the Hugging Face Hub.
# - No argparse
# - Uses Python API directly
# - One mode per run
#
# HOW TO USE
# 1) Edit the USER CONFIG section.
# 2) Pick ONE MODE.
# 3) Run:  python windows_hf_upload_probe.py
# 4) Change MODE and rerun with a fresh file if you want a fair comparison.
#
# MODES
# - "xet-default"   : normal current path
# - "xet-fixed1"    : Xet enabled, adaptive concurrency disabled, upload concurrency pinned to 1
# - "http-fallback" : disable hf_xet and force HTTP fallback
#
# NOTES
# - For a fair A/B test, do not compare multiple modes on a file whose bytes are already known to the Hub.
# - This script uploads one file. It does not shell out to `hf`.
# - Environment variables are set BEFORE importing huggingface_hub.

from __future__ import annotations

import os
import sys
import json
import time
import traceback
import platform
from pathlib import Path

# =========================
# USER CONFIG
# =========================

MODE = "xet-default"  # "xet-default" | "xet-fixed1" | "http-fallback"

LOCAL_FILE = r"D:\models\my-model.Q4_K_M.gguf"
REPO_ID = "your-username-or-org/your-test-repo"
REPO_TYPE = "model"   # "model" | "dataset" | "space"
TOKEN = None          # None -> use cached login or HF_TOKEN env var

# Put each test in a separate remote path so results do not overwrite each other.
# This does NOT defeat Xet dedupe. It only keeps commits separate and readable.
REMOTE_SUBDIR = "troubleshoot"

# Optional. Set this to a fast local SSD/NVMe path with lots of free space.
# Leave as None to keep the default cache location.
HF_XET_CACHE_DIR = r"D:\hf-xet-cache"

# Optional. Xet logs will go here for Xet modes.
XET_LOG_DIR = r"D:\hf-xet-logs"

# Create the repo automatically if it does not exist yet.
CREATE_REPO_IF_MISSING = True

# =========================
# ENV SETUP BEFORE IMPORT
# =========================

os.environ.setdefault("HF_DEBUG", "1")
os.environ.setdefault("HF_HUB_VERBOSITY", "debug")

if TOKEN:
    os.environ["HF_TOKEN"] = TOKEN

if HF_XET_CACHE_DIR:
    os.environ["HF_XET_CACHE"] = HF_XET_CACHE_DIR

# Put Xet logs in files instead of only console noise.
# Use a directory path ending with "\" on Windows to make the intent obvious.
if XET_LOG_DIR:
    xet_log_dir = Path(XET_LOG_DIR)
    xet_log_dir.mkdir(parents=True, exist_ok=True)
    os.environ["HF_XET_LOG_DEST"] = str(xet_log_dir) + "\\"

# Clean mode-specific variables first so reruns in the same shell are less confusing.
for key in [
    "HF_HUB_DISABLE_XET",
    "HF_XET_FIXED_UPLOAD_CONCURRENCY",
    "HF_XET_CLIENT_ENABLE_ADAPTIVE_CONCURRENCY",
]:
    os.environ.pop(key, None)

if MODE == "http-fallback":
    os.environ["HF_HUB_DISABLE_XET"] = "1"
elif MODE == "xet-fixed1":
    os.environ["HF_XET_CLIENT_ENABLE_ADAPTIVE_CONCURRENCY"] = "false"
    os.environ["HF_XET_FIXED_UPLOAD_CONCURRENCY"] = "1"
elif MODE == "xet-default":
    pass
else:
    raise ValueError(f"Unknown MODE: {MODE!r}")

# =========================
# IMPORTS AFTER ENV SETUP
# =========================

from huggingface_hub import HfApi, __version__ as huggingface_hub_version  # noqa: E402


def try_import_hf_xet_version() -> str | None:
    try:
        import hf_xet  # type: ignore
        return getattr(hf_xet, "__version__", "unknown")
    except Exception:
        return None


def bytes_to_gb(n: int) -> float:
    return n / (1024 ** 3)


def build_remote_path(local_path: Path, mode: str) -> str:
    timestamp = time.strftime("%Y%m%d-%H%M%S")
    return f"{REMOTE_SUBDIR}/{mode}/{timestamp}/{local_path.name}"


def main() -> int:
    local_path = Path(LOCAL_FILE)

    if not local_path.is_file():
        print(f"[ERROR] File not found: {local_path}")
        return 2

    file_size = local_path.stat().st_size
    hf_xet_version = try_import_hf_xet_version()

    print("=" * 80)
    print("Hugging Face upload probe")
    print("=" * 80)
    print(f"mode                : {MODE}")
    print(f"local_file          : {local_path}")
    print(f"file_size_bytes     : {file_size}")
    print(f"file_size_gb        : {bytes_to_gb(file_size):.3f}")
    print(f"repo_id             : {REPO_ID}")
    print(f"repo_type           : {REPO_TYPE}")
    print(f"huggingface_hub     : {huggingface_hub_version}")
    print(f"hf_xet              : {hf_xet_version}")
    print(f"python              : {sys.version.split()[0]}")
    print(f"platform            : {platform.platform()}")
    print(f"HF_HUB_DISABLE_XET  : {os.environ.get('HF_HUB_DISABLE_XET')}")
    print(f"HF_XET_CACHE        : {os.environ.get('HF_XET_CACHE')}")
    print(f"HF_XET_LOG_DEST     : {os.environ.get('HF_XET_LOG_DEST')}")
    print(f"HF_DEBUG            : {os.environ.get('HF_DEBUG')}")
    print("=" * 80)

    api = HfApi(token=TOKEN)

    if CREATE_REPO_IF_MISSING:
        print("[INFO] Ensuring repo exists...")
        api.create_repo(
            repo_id=REPO_ID,
            repo_type=REPO_TYPE,
            exist_ok=True,
            private=True,  # change if you want
        )

    remote_path = build_remote_path(local_path, MODE)
    commit_message = f"upload probe: {MODE} / {local_path.name}"

    print(f"[INFO] Remote path: {remote_path}")
    print("[INFO] Starting upload...")

    started = time.perf_counter()
    result = {
        "ok": False,
        "mode": MODE,
        "repo_id": REPO_ID,
        "repo_type": REPO_TYPE,
        "local_file": str(local_path),
        "remote_path": remote_path,
        "file_size_bytes": file_size,
        "file_size_gb": round(bytes_to_gb(file_size), 3),
        "huggingface_hub": huggingface_hub_version,
        "hf_xet": hf_xet_version,
        "python": sys.version.split()[0],
        "platform": platform.platform(),
        "env": {
            "HF_HUB_DISABLE_XET": os.environ.get("HF_HUB_DISABLE_XET"),
            "HF_XET_FIXED_UPLOAD_CONCURRENCY": os.environ.get("HF_XET_FIXED_UPLOAD_CONCURRENCY"),
            "HF_XET_CLIENT_ENABLE_ADAPTIVE_CONCURRENCY": os.environ.get("HF_XET_CLIENT_ENABLE_ADAPTIVE_CONCURRENCY"),
            "HF_XET_CACHE": os.environ.get("HF_XET_CACHE"),
            "HF_XET_LOG_DEST": os.environ.get("HF_XET_LOG_DEST"),
            "HF_DEBUG": os.environ.get("HF_DEBUG"),
        },
    }

    try:
        # Direct Python API upload.
        returned_path = api.upload_file(
            path_or_fileobj=str(local_path),
            path_in_repo=remote_path,
            repo_id=REPO_ID,
            repo_type=REPO_TYPE,
            commit_message=commit_message,
        )

        elapsed = time.perf_counter() - started
        result["ok"] = True
        result["elapsed_seconds"] = round(elapsed, 2)
        result["returned_path"] = str(returned_path)

        print("[OK] Upload finished")
        print(f"[OK] elapsed_seconds: {elapsed:.2f}")
        print(f"[OK] returned_path  : {returned_path}")

    except Exception as e:
        elapsed = time.perf_counter() - started
        result["ok"] = False
        result["elapsed_seconds"] = round(elapsed, 2)
        result["error_type"] = type(e).__name__
        result["error"] = str(e)
        result["traceback"] = traceback.format_exc()

        print("[ERROR] Upload failed")
        print(f"[ERROR] elapsed_seconds: {elapsed:.2f}")
        print(f"[ERROR] {type(e).__name__}: {e}")
        print(traceback.format_exc())

    # Save a machine-readable report next to the script.
    report_name = f"hf_upload_probe_{MODE}_{time.strftime('%Y%m%d-%H%M%S')}.json"
    report_path = Path(__file__).with_name(report_name)
    report_path.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")

    print(f"[INFO] Report saved to: {report_path}")

    # Exit nonzero on failure for easy automation later.
    return 0 if result["ok"] else 1


if __name__ == "__main__":
    raise SystemExit(main())

How to use it

Set MODE = "xet-default" first and run it once. Then rerun with MODE = "http-fallback". Then rerun with MODE = "xet-fixed1". For a real A/B, use a file that is genuinely new to the Hub for each run, or at least a fresh test repo and fresh content. Xet is designed to avoid re-uploading known chunks, so repeated tests on the exact same bytes can make a broken path look healthy. (Hugging Face)

How to read the results

  • xet-default fails or stalls, but http-fallback works
    That strongly points to the Xet path on Windows. HF_HUB_DISABLE_XET is the official way to force that fallback. (Hugging Face)

  • xet-default fails, xet-fixed1 works
    That suggests adaptive concurrency or parallel upload behavior is part of the problem. Xet’s docs say adaptive concurrency is on by default, and fixed concurrency bypasses it. (Hugging Face)

  • All three fail the same way
    Then the problem is more likely a broader Windows upload-path issue, a local disk/cache problem, or a narrower backend interaction, not just Xet concurrency. The open Windows issue about uploads with new data above about 2 GB getting stuck is still relevant context. (GitHub)

  • http-fallback also fails, but a Linux or WSL run works
    That points even more strongly to Windows-specific behavior. The open issue and linked fix PR are explicitly about multipart uploads on Windows. (GitHub)

Yeah well I tried an old venv that I haven’t touched since the end of February, meaning no dependencies updates and I only uploaded 1 GGUF file, nothing has been updated since the end of February and yep same exact issue, very slow speed.

1 Like

Oh… In that case, it’s highly likely that the issue is either due to some change on Hugging Face’s servers or that the connection is being throttled somewhere along the route, including the HF servers…