All writing

Why Windows Search stalls for 30 seconds after a reboot

Windows Search went blank for 30 seconds after every reboot on four PCs. The cause was a deadlock between the search indexer and Dropbox, and one NTFS attribute fixes it.

For months, Windows Search behaved the same way on four different Windows 11 PCs: after every reboot the search box sat blank for 20 to 30 seconds before any result appeared. Different hardware, different user profiles, the same fault. The usual remedies changed nothing, so I attached a debugger to the search process and watched what it was waiting on.

The symptom#

The primary test machine was a Windows 11 Pro box, build 26100, with a Ryzen 5 2400G and 14 GB of RAM. The same stall reproduced on three others: a laptop, a workstation, and a family member's desktop. They had different hardware and different Windows builds, and the one thing they shared was Dropbox running with Smart Sync.

The behaviour was identical everywhere. Press Win+S, type a few letters, and stare at an empty pane for 20 to 30 seconds. Then results appear, and search works normally until the next reboot. Rebuilding the index, disabling Bing, re-registering the search packages, and resetting the WSearch service, the standard forum advice, made no difference. The stall always returned after a restart.

The useful clue was a timing coincidence: search began working at the exact moment the Dropbox tray icon finished loading, every time. Once I noticed it on the primary machine, I confirmed the same correlation on the other three. That pointed at a boot-time dependency nobody was looking at.

Instrumenting the search process#

A blank search box has a hundred possible causes, so I set out to watch what SearchHost.exe was actually doing during those seconds. I used Frida, a toolkit that hooks functions in a running process, together with a small Python tracer built on the Windows debugging APIs: NtQueryInformationThread for each thread's start address, EnumProcessModulesEx to map addresses to modules, and MiniDumpWriteDump for offline analysis.

The tracer kills SearchHost to force a cold start, reattaches when it respawns, triggers Win+S, types a query, and records every blocking call per thread for 20 seconds. The hooks sat on the calls a thread uses to wait: WaitForSingleObject, WaitForMultipleObjects, MsgWaitForMultipleObjectsEx, NtAlpcSendWaitReceivePort, and CoCreateInstance. One Frida 17 detail cost me an hour: the static Module.getExportByName is gone, and you now resolve exports through Process.getModuleByName().

SearchHost runs 46 threads, but the result was lopsided. Two threads accounted for 21 of the 22 seconds of total blocked time; the rest were idle thread-pool workers or waiting on those two.

Top blocking threads over a 20-second trace
TID 8380:   12,597 ms   shcore.dll+0x47c50    UI message pump
TID 13628:   8,445 ms   tquery.dll+0x153350   search query engine

The minifilter stack#

To see why those reads were slow, it helps to know how file access works on Windows. Every ReadFile passes through a stack of minifilters — drivers that sit in the I/O path and can inspect, delay, or block each read and write. Each one occupies a fixed altitude, Microsoft's term for its position in the stack; a higher altitude sees the request first. Microsoft hands out altitude ranges by category, so antivirus gets 320000 to 329999 and cloud storage gets 180000 to 189999, and you register your altitude with them before shipping a driver.

FilterAltitudePurpose
bindflt409800Container binding
WdFilter328010Windows Defender (antivirus band)
storqosflt244000Storage QoS
CldFlt180451Cloud Files (Dropbox, OneDrive)
bfs150000Basic filesystem
FileInfo40500File metadata
Two filters matter here: WdFilter at 328010 is Defender, and CldFlt at 180451 is the Cloud Files filter that Dropbox uses.

Four problems, not one#

The stall was four independent problems that overlapped at boot and made each other worse.

The first was Defender amplifying I/O. WdFilter intercepts every open and read, so indexing one source file turns into four operations: the indexer reads it, Defender scans that read, the extracted text is written to the index, and Defender scans the write. Across thousands of .rs, .js, and .py files this becomes sustained disk thrashing, and Defender accumulated 270 seconds of CPU against the indexer's 250 on the same files. An exclusion existed for the old C:\Users\Marty\ profile but not the current C:\Users\marty.CHOPIN\, so every dev file was scanned twice. An exclusion for the indexer process (Add-MpPreference -ExclusionProcess SearchIndexer.exe) dropped Defender's share to 0.02 seconds over a three-second window.

The second was the size of the index. Windows keeps it in a SQLite database, Windows.db, which on this machine had grown to 1,641 MB against a normal 100 to 300 MB. The bloat was full-text content indexing of source files. In the trace, tquery.dll (the query engine) blocked for 8.4 seconds in a single WaitForMultipleObjects while scanning that database. Registering the null persistent handler for code extensions tells the indexer to store filenames only and skip content extraction; files stay findable by name, and the database fell to about 50 MB.

The third was WebView2. Windows 11's search UI is a web app that SearchHost renders inside an embedded Chromium browser, and every boot it spawns six msedgewebview2.exe processes using around 370 MB. The trace caught it polling registry flags such as IsWebView2 and SnrBundleVersion more than 21 times each during startup. On build 26100 there is no supported way to disable it, so this is a fixed cost of roughly five seconds.

Those three added up to perhaps 15 seconds. The rest, and the part that tracked the Dropbox tray icon, came from the fourth.

The root cause: a blocked cloud read#

Dropbox Smart Sync uses the Windows Cloud Files API to show placeholder files: entries that appear with their real name and size but hold no local content. Each carries the attribute FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS (0x400000), and reading the content triggers a download, called hydration, through CldFlt. On this machine all 9,234 Dropbox files were placeholders.

Every sampled Dropbox file is a placeholder
Get-ChildItem "$env:USERPROFILE\Dropbox" -Recurse -File |
  Select -First 200 | ForEach {
    [uint32][IO.File]::GetAttributes($_.FullName) -band 0x400000
  } | Group | Select Count, Name

Count  Name
200    True     # all 200 sampled files: RECALL_ON_DATA_ACCESS

At boot, WSearch starts before Dropbox has finished loading. The indexer crawls the Dropbox folder and tries to read each file for content. In the filter stack the read reaches CldFlt, which recognises a placeholder and asks the sync provider to fetch the data, but Dropbox has not connected to the filter yet, so there is no provider to answer, and the read blocks until it times out, up to 60 seconds.

A single indexer read, blocked at CldFlt
SearchIndexer ReadFile("proposal.docx")
  at 328010  WdFilter    Defender scans the read
  at 180451  CldFlt      placeholder: issues CF_CALLBACK_TYPE_FETCH_DATA
                         callback sent to Dropbox sync provider
                         Dropbox still starting, no provider connected
                         I/O blocks, up to 60s timeout
  at 150000  bfs         never reached while blocked

This is a deadlock in the practical sense: the read cannot complete until Dropbox connects, and while it waits it holds a filter-host thread. SearchIndexer.exe lives under %systemroot%, which lets it bypass the cloud-file access denial (STATUS_CLOUD_FILE_ACCESS_DENIED, 0xC000CF18) that stops most services from triggering hydration, so it tries to download every placeholder. As blocked reads pile up, the thread pool saturates and Windows starts killing the hosts: the Application log held 66 Event ID 10024 entries, "the filter host process did not respond and is being forcibly terminated." It clears the moment Dropbox connects its provider to CldFlt and the pending reads complete, which is exactly the tray-icon moment I had noticed.

The orphan sync root variant#

The mitigations below appeared to fix the machine, and then three days later it regressed: Win+S blank for 30 seconds again, this time with Dropbox already running for hours. The cause was different now, a stale registration left by a profile migration rather than a boot race.

When a Windows account is renamed, the old profile directory stays on disk and its Dropbox sync root stays in the registry. Here an old Marty account had been replaced by marty.CHOPIN. The old C:\Users\Marty\ tree survived, and under SyncRootManager so did a sync root for a ghost SID ending in -1000 that no longer existed in ProfileList. When CldFlt resolves a placeholder under that tree, it finds a registered provider that no process will ever connect to, and issues a fetch that never returns. This is the boot deadlock made permanent. Every subfolder was a cloud reparse point (tag 0x9000601A, IO_REPARSE_TAG_CLOUD_A), so any enumeration that crossed the tree blocked, and deleting a file in Explorer took minutes while the shell waited for a provider to dehydrate it.

The fix has two steps, and most write-ups miss the second:

Remove the orphan tree, then its registration
# Explorer delete hangs on cloud dehydration callbacks; rd bypasses the shell
cmd /c "rd /s /q C:\Users\Marty\Dropbox"

# unregister the ghost sync root - the step that actually stops the blocking
Remove-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager\Dropbox!S-1-5-21-1443736254-2868974255-1052510127-1000!dbid:*" -Recurse

Restart-Service WSearch -Force

Deleting the folder alone reclaims space but leaves the registration, so CldFlt still points at a dead path. Removing the ghost sync root is what stops the blocking. Any machine that has been through a profile rename is a candidate, whether or not the boot version ever fired.

What helps#

Each problem has a mitigation, and they differ in how long they last.

ProblemFixSurvives reboot?
CldFlt / Dropbox blockattrib +I on the Dropbox folder (NTFS NOT_CONTENT_INDEXED)Yes
Defender I/O amplificationAdd-MpPreference -ExclusionProcess SearchIndexer.exeYes
1.6 GB index bloatNull persistent handler on code extensionsUntil a Windows update
WebView2 cold startScheduled task pre-warms search at loginYes
The first row is the important one; the others shorten the stall but do not remove its cause.

FILE_ATTRIBUTE_NOT_CONTENT_INDEXED (0x2000) on the Dropbox tree tells the indexer to record names and metadata but skip content extraction, which is the step that triggers hydration. The files stay searchable by name, and because the attribute is NTFS metadata it survives reboots, index rebuilds, and updates.

Mark the Dropbox tree not-content-indexed
attrib +I "C:\Users\%USERNAME%\Dropbox" /S /D

There is a stronger version. attrib +I still lets the indexer walk the tree and touch each placeholder's metadata on every crawl, which keeps the CldFlt path warm. Removing the Dropbox tree from the indexer's scope stops even that. The documented route is ISearchCrawlScopeManager::AddUserScopeRule with the include flag set to zero, but on build 26100 that interface returns E_NOINTERFACE for both published IIDs, and the registry key it maps to is locked to TrustedInstaller. The working approach is to run the write from a one-shot scheduled task as SYSTEM, which has the rights the key demands. A folder excluded this way is no longer searchable from Win+S at all, which is why I stopped tuning the built-in indexer and started replacing it.

A search tool that never opens a file#

voidtools Everything indexes a million files in seconds because it never enters the filter stack. Instead of opening files, it opens the volume itself and reads the NTFS Master File Table, the filesystem's own index of every file, through a device control call.

Read the MFT through a volume handle
// open a volume handle, not a file handle
HANDLE hVol = CreateFileW(L"\\\\.\\C:", GENERIC_READ,
    FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);

// walk the MFT record by record: name, parent, size, timestamps
MFT_ENUM_DATA_V0 med = { 0, 0, maxUsn };
while (DeviceIoControl(hVol, FSCTL_ENUM_USN_DATA,
        &med, sizeof(med), buf, bufSize, &bytesReturned, NULL)) {
    // no file opened, no content read, no minifilter traversed
}

A FSCTL_ENUM_USN_DATA call on a volume handle is a request to the NTFS driver, not a file open, so no minifilter runs: no Defender scan, no CldFlt hydration, no content extraction. Each record yields a name, a parent, and a size, which is all a filename search needs. For live updates it watches the USN change journal (FSCTL_READ_USN_JOURNAL) on the same handle.

EverythingWindows Search
Initial scan~1 second (120k files)Minutes to hours
Content indexingNone, filenames onlyFull text via IFilter plugins
Minifilter stackBypassed (volume-level call)Every file traverses it
DatabaseCustom binary, under 50 MBSQLite, often over 1 GB
Everything reads only metadata from the volume; the built-in indexer opens and reads each file.

That mechanism is the durable fix, because configuration knobs keep getting undone: every new placeholder, profile rename, or feature update can hand file I/O back to CldFlt. So the direction the work took is a replacement engine, wired into the native Win+S box so nothing downstream notices the swap. It is a Rust daemon, supersearch, that reads the MFT on startup, follows the USN journal, and keeps a trigram index in flat storage. For 877,000 files on the Ryzen 2400G it uses about 181 MB of RAM, indexes the volume in three seconds, and answers a query in under two milliseconds.

Getting results into the existing search UI took some care. SearchHost does not go through the documented ISearchManager; it calls CoCreateInstance directly on the Windows Search OLE DB provider (CLSID {9e175b8b-f52a-11d8-b9a5-505054503030}, in tquery.dll) and builds an SQL query as a string first.

The query SearchHost builds for a simple search
SELECT TOP 15 "System.ItemNameDisplay", path, "System.Search.Rank"
FROM SystemIndex
WHERE CONTAINS(System.ItemNameDisplay, '"notepad*"') RANK BY COERCION(ABSOLUTE, 960)
ORDER BY "System.Search.Rank" DESC

Hijacking that CLSID is a dead end: the key is protected by Windows Resource Protection and cannot be rewritten even as SYSTEM. What works is to inject a small DLL into SearchHost with CreateRemoteThread and LoadLibraryW (it must sit in System32 for the AppContainer sandbox to load it) and hook SysAllocString in oleaut32 with a short jump. Because SearchHost builds its query string through that call, the hook reads the search term, forwards file searches to the daemon, and lets everything else (email, web, settings) fall through to the real query engine.

The stall looked like one broken component and was in fact four, tied together by a cloud filter waiting on a program that had not started yet. The mitigations help, and the single NTFS attribute on the Dropbox folder removes the worst of it for good. The lasting lesson is about layers: the built-in indexer is slow because it reads files, and on a modern Windows machine reading a file means running a gauntlet of drivers. Read the filesystem's own index instead, and the problem disappears.