Cursor Settings → Indexing reads “Indexing… 3,432 of 1,200,000 files.” Half an hour later the count hasn’t moved; sometimes the spinner just turns at 0%. This matters because Cursor’s semantic search only switches on at 80% indexed (per Cursor’s indexing docs) — until then, @Codebase answers and Agent file-finding run blind.
The single most common cause isn’t a Cursor bug. It’s a .cursorignore that doesn’t cover node_modules and build outputs, so a million generated files got thrown into the embedder. The second-most-common cause in 2026 is new: a corporate SSL-inspection proxy (Zscaler, Netskope) silently breaking the HTTP/2 connection Cursor uses to upload embeddings. Below: how to tell which class you’re in, then exclude or reconfigure precisely.
TL;DR
- Indexing stalls at a huge file count → your
.cursorignoreis missing build/cache dirs. Fix Step 1. - Spinner stuck at 0% or “Handshake Failed”, chat and Tab also broken → corporate proxy blocking HTTP/2. Turn on Disable HTTP/2 and fully restart Cursor (Step 2).
- Only happens on one repo → ignore config, symlinks, or scale. Happens on every repo → network/proxy or a broken Cursor install.
- Semantic search needs 80% indexed before it works (per Cursor’s indexing docs); the index then auto-syncs every 5 minutes on only the changed files, and Cursor drops the cached index after 6 weeks of inactivity.
Find which class of failure you have
Run these at the repo root first — they tell you which fix to jump to.
git ls-files | wc -l # files Cursor would index if .cursorignore = .gitignore
find . -type f 2>/dev/null | wc -l # everything on disk, including ignored
du -sh node_modules .next dist build coverage target 2>/dev/null | sort -h
find . -type l 2>/dev/null | head -20 # symlinks (loop suspects)
df -h . # mount type (network/sync drive?)
If line 2 is far bigger than line 1, you have un-ignored generated files (cause 1). If a symlink points up the tree, suspect a loop (cause 2). If df shows SMB/NFS or an iCloud/Dropbox path, suspect IO latency (cause 5). If indexing is stuck at 0% and chat/Tab are dead, skip to the proxy fix (cause 6).
Common causes
1. node_modules / dist / .next / build not ignored
Cursor honors .gitignore and its own default ignore list, but plenty of teams’ .gitignore lists node_modules and misses dist/, coverage/, .next/, .turbo/, __pycache__/, venv/. The .next/cache/webpack directory alone can hold 500k files in a Next.js monorepo. The embedder will dutifully try to chunk every one.
How to judge: the biggest directory from the du -sh line above that is not already in .gitignore is your suspect.
2. Symlink loops
packages/web/node_modules/internal-pkg -> ../../shared, and shared symlinks back. The scanner recurses forever.
How to judge:
find . -type l -exec ls -l {} \; 2>/dev/null | grep -E "\.\./.*\.\./"
Any symlink pointing back up to a parent directory is a candidate.
3. Repo exceeds practical indexing scale
There’s no published hard file cap, but Cursor’s own guidance is that “large repositories with tens of thousands of files can take hours” and that the fix “for most teams is a tight .cursorignore, not killing the index.” Per-file, anything over ~5,000 lines (minified bundles, generated protobuf/GraphQL/OpenAPI clients) hurts the tokenizer and is wasted index space.
How to judge: if find . -type f | wc -l is north of 100k after ignores, you’re past the comfortable zone — narrow the workspace (Step 4) rather than waiting.
4. Permission denied
A directory owned by root (a system path or Docker volume) that the Cursor process can’t read. The indexer hangs waiting on it.
How to judge:
find . -not -readable 2>/dev/null | head
5. Network drive / WSL boundary
Project lives on SMB / NFS / Dropbox / iCloud sync — per-file IO latency jumps to 100ms+, and indexer throughput drops by two orders of magnitude. A 30k-file repo that indexes in 4 minutes on a local SSD can take hours over a synced folder.
How to judge: df -h . for the mount type; a path like /mnt/c/... (WSL) or ~/Library/Mobile Documents/... (iCloud) is the tell.
6. Corporate proxy blocking HTTP/2 (the 2026 culprit)
Cursor uses HTTP/2 bidirectional streaming for embeddings, chat, and Tab. SSL-inspection proxies like Zscaler (per Cursor’s network-configuration docs, “the most widely used proxy with this limitation”) and Netskope frequently buffer or break HTTP/2 streams, so the index requests get truncated, garbled, or time out — and the UI just spins at 0% or throws “Handshake Failed.” Cursor is supposed to fall back to HTTP/1.1 server-sent events automatically, but the auto-detect often misses an inspection proxy, which is why you flip the toggle by hand in Step 2. The tell: indexing, chat, and Tab autocomplete are all broken at once, on every repo, and only on the corporate network.
How to judge: does it work fine on a personal hotspot but stall on office Wi-Fi/VPN? That’s the proxy.
7. Cursor’s indexer service hung
The extension host crashed or the index worker died, but the UI doesn’t update.
How to judge: Cmd+Shift+P → “Developer: Show Running Extensions” — Cursor’s index worker shows as unresponsive.
Before you start
- Confirm whether it’s this one repo or every repo. Every repo usually points at the proxy (cause 6) or a broken Cursor install.
- Commit
.cursorignorebefore editing so you can roll back. - Note Cursor version, repo file count, OS, and subscription tier (Hobby / Pro / Pro+ / Business).
- Check status.cursor.com — Cursor has had server-side indexing incidents that no local fix will solve.
Shortest fix path
Ordered by impact.
Step 1: Write a proper .cursorignore
At repo root, create or edit .cursorignore. It uses gitignore syntax (*, **, ?, ! to re-include, # for comments):
# Dependencies
node_modules
.pnpm
.yarn
vendor
__pycache__
venv
.venv
# Build output
dist
build
out
.next
.nuxt
.turbo
.svelte-kit
coverage
target
.gradle
# Caches
.cache
.parcel-cache
.eslintcache
*.tsbuildinfo
# Data / binaries (waste of embedding budget)
*.parquet
*.bin
*.gz
*.zip
*.sqlite
*.min.js
*.min.css
# Logs
*.log
logs
Know the difference between the two ignore files (from the Cursor ignore-file reference):
| File | Blocks indexing | Still usable as manual @ context / by Agent | Use it for |
|---|---|---|---|
.cursorignore | Yes | No — fully blocked from semantic search, Tab, Agent, Inline Edit, @ mentions | .env, secrets, node_modules, build junk |
.cursorindexingignore | Yes | Yes — file is hidden from search but you can still attach it | Large generated files / vendored code you occasionally need to read |
In ~90% of cases .cursorignore is the only one you need. Note: the terminal and MCP server tools an Agent runs cannot honor .cursorignore, so it is not a hard secret boundary.
Step 2: If chat and Tab are also dead — fix HTTP/2
If indexing is stuck at 0% and the whole app is unresponsive on a corporate network, force the HTTP/1.1 fallback. Open settings (Cmd/Ctrl + ,), search http2, and turn on the Disable HTTP/2 toggle. The equivalent in settings.json (Cmd+Shift+P → “Preferences: Open User Settings (JSON)”) is:
"cursor.general.disableHttp2": true
Then fully quit and reopen Cursor — a “Developer: Reload Window” does not reload the network stack, so the change won’t take until you restart the app. Cursor now uses HTTP/1.1 server-sent events: streaming is slightly slower, but it passes through SSL-inspection proxies. Reports put this at fixing roughly 70% of corporate-network indexing failures. If your org runs its own proxy, the cleaner long-term fix is to ask IT to whitelist *.cursor.com from SSL inspection rather than downgrade every developer to HTTP/1.1.
Step 3: Trigger a clean reindex
Open Cursor Settings → Indexing (newer builds label it Indexing & Docs) and use Resync Index; if that doesn’t take, use Delete and Reindex. Then Cmd+Shift+P → “Developer: Reload Window.” Resetting the index touches no source code — you only drop the cached embeddings, which rebuild on next open (a few minutes on a normal repo). Watch progress; remember search lights up at 80%.
Step 4: Break symlink loops
For each parent-pointing symlink found earlier, either add it to .cursorignore or convert it to a native npm / pnpm / yarn workspace link instead of a raw filesystem symlink.
Step 5: Narrow the working directory
Don’t open Cursor at a 50k-file monorepo root. File → Open Folder → pick the package you actually work in this week (apps/web). The indexing surface shrinks by an order of magnitude and search quality improves because there’s less noise.
Step 6: Move off network drive / sync folder
git clone to a native local path (~/repo on APFS, WSL ext4, or native Linux). Never keep an active project inside Dropbox / iCloud / a network mount.
Step 7: Nuclear option — clear workspace storage
# macOS / Linux
rm -rf ~/Library/Application\ Support/Cursor/User/workspaceStorage/*
# Windows (PowerShell)
# Remove-Item -Recurse -Force $env:APPDATA\Cursor\User\workspaceStorage\*
This wipes every workspace’s index cache; every repo reindexes on next open. Last resort only.
How to verify the fix
- Restart Cursor and reproduce, to confirm it isn’t transient session state.
- Indexing reaches 100% in a reasonable time (a normal repo on a local SSD is minutes, not hours).
- Open Cursor Settings → Indexing & Docs → View included files to see exactly what got indexed — the count should match
git ls-files | wc -lminus your.cursorignorematches, andnode_modules/ build dirs should be absent. - Ask Composer “List every file under
src/api/” — a correct, complete list proves the index actually covers the tree.
If it still fails
- Shrink the repro: open a fresh empty repo and see if that also stalls. If it does, the problem is Cursor/network, not your repo.
- Roll back the most recent Cursor upgrade or
.cursorignorechange. - Search forum.cursor.com for “indexing never completes” or “Handshake Failed”; include file count, OS, and your ignore content.
- Grab Help → Toggle Developer Tools → Console indexing logs and post them to Bug Reports.
Prevention
- First thing on a new repo: drop in a
.cursorignore. Don’t wait to be bitten. - Keep
.cursorignoreand.gitignoredifferentiated: generated types / schemas the model genuinely needs stay indexed;node_modulesand build output always out. - Don’t commit datasets (parquet / sqlite / huge JSON) — use git-lfs or S3, and ignore them either way.
- Always run on native filesystems; never iCloud / Dropbox / network mounts.
- On corporate hardware, turn on Disable HTTP/2 (or get IT to whitelist
*.cursor.comfrom SSL inspection) before you hit the wall. - Run
git ls-files | wc -lquarterly. Past ~50k files, consider splitting the monorepo or scoping Cursor to one package.
FAQ
Why does Cursor say indexing is done but @Codebase still gives wrong answers?
Search only activates at 80% completion, so a “done” badge can precede full coverage. Also confirm the file isn’t in .cursorignore — that blocks it from search entirely, not just from indexing.
Does deleting the index lose any of my code? No. The index is only cached embeddings stored separately from your files. Delete and Reindex rebuilds them on next open and never modifies source. (Cursor also drops the cached index by itself after 6 weeks of no activity on a repo, and rebuilds it the next time you open the project — that one-time rebuild is expected, not a bug.)
What’s the difference between .cursorignore and .cursorindexingignore?
.cursorignore fully blocks a file from every AI feature (search, Tab, Agent, Inline Edit, @). .cursorindexingignore only keeps it out of the search index — you can still attach it manually as context. Use the first for secrets and junk, the second for large generated files you occasionally reference.
Indexing works at home but stalls on the office network — why?
Almost always an SSL-inspection proxy (Zscaler / Netskope) that buffers or breaks HTTP/2. Turn on Disable HTTP/2 in Settings (search http2), or set "cursor.general.disableHttp2": true, then fully quit and reopen Cursor — a window reload alone won’t reload the network stack.
How often does Cursor re-index after the first build? Automatically every 5 minutes, and only on files that changed — so a one-time large initial build is normal, but steady-state syncs are cheap.