ReadDirectoryChangesW
completion routine
file monitoring
Windows API
programming tutorial

How to use ReadDirectoryChangesW method with completion routine?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

ReadDirectoryChangesW can watch a directory for file-system activity such as creates, deletes, renames, and content changes. When you use it with a completion routine, the call becomes asynchronous, so your thread can wait efficiently instead of blocking on one directory read.

The part that usually trips people up is not the function call itself. It is the surrounding Win32 I/O model: the directory handle must be opened for overlapped I/O, the buffer must stay alive until completion, and the thread must enter an alertable wait so the completion routine can actually run.

The Core Setup

To use ReadDirectoryChangesW with a completion routine, you need all of these pieces:

  • a directory handle opened with FILE_FLAG_BACKUP_SEMANTICS and FILE_FLAG_OVERLAPPED
  • a buffer that remains valid until the asynchronous request finishes
  • an OVERLAPPED structure that also remains valid
  • a completion routine with the CALLBACK signature
  • an alertable wait such as SleepEx(INFINITE, TRUE)

A stack buffer is usually the wrong choice because the function returns immediately while the I/O is still pending.

Open the Directory Correctly

First, open the directory handle with CreateFileW:

c
1HANDLE dir = CreateFileW(
2    L"C:\\watched",
3    FILE_LIST_DIRECTORY,
4    FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
5    NULL,
6    OPEN_EXISTING,
7    FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
8    NULL
9);

If that handle is invalid, nothing else will work. FILE_FLAG_BACKUP_SEMANTICS is required because you are opening a directory, not a normal file.

Keep Context with the OVERLAPPED

A convenient pattern is to store the OVERLAPPED structure and the notification buffer inside one context object:

c
1#include <windows.h>
2#include <stdio.h>
3
4#define BUFFER_SIZE 16384
5
6typedef struct WatchContext {
7    OVERLAPPED overlapped;
8    HANDLE directory;
9    BYTE buffer[BUFFER_SIZE];
10} WatchContext;

This way, when the completion routine receives LPOVERLAPPED, you can recover the full watch state with CONTAINING_RECORD.

Queue the Asynchronous Read

Create a helper that starts or restarts monitoring:

c
1BOOL BeginWatch(WatchContext *ctx);
2
3VOID CALLBACK OnDirectoryChange(
4    DWORD errorCode,
5    DWORD bytesTransferred,
6    LPOVERLAPPED overlapped
7);
8
9BOOL BeginWatch(WatchContext *ctx) {
10    ZeroMemory(&ctx->overlapped, sizeof(ctx->overlapped));
11
12    return ReadDirectoryChangesW(
13        ctx->directory,
14        ctx->buffer,
15        sizeof(ctx->buffer),
16        TRUE,
17        FILE_NOTIFY_CHANGE_FILE_NAME |
18        FILE_NOTIFY_CHANGE_DIR_NAME |
19        FILE_NOTIFY_CHANGE_LAST_WRITE,
20        NULL,
21        &ctx->overlapped,
22        OnDirectoryChange
23    );
24}

Notice the key points:

  • 'lpBytesReturned is NULL for overlapped use here'
  • the buffer belongs to ctx, not a local variable
  • the same context can be re-armed after each notification batch

Process Notifications in the Completion Routine

When the I/O completes, Windows calls your completion routine on the same thread that entered the alertable wait:

c
1VOID CALLBACK OnDirectoryChange(
2    DWORD errorCode,
3    DWORD bytesTransferred,
4    LPOVERLAPPED overlapped
5) {
6    WatchContext *ctx = CONTAINING_RECORD(overlapped, WatchContext, overlapped);
7
8    if (errorCode == ERROR_OPERATION_ABORTED) {
9        return;
10    }
11
12    if (errorCode != ERROR_SUCCESS) {
13        fprintf(stderr, "ReadDirectoryChangesW failed: %lu\n", errorCode);
14        return;
15    }
16
17    BYTE *base = ctx->buffer;
18    FILE_NOTIFY_INFORMATION *info = (FILE_NOTIFY_INFORMATION *)base;
19
20    while (bytesTransferred > 0) {
21        wprintf(L"Action=%lu Name=%.*ls\n",
22                info->Action,
23                info->FileNameLength / sizeof(WCHAR),
24                info->FileName);
25
26        if (info->NextEntryOffset == 0) {
27            break;
28        }
29
30        info = (FILE_NOTIFY_INFORMATION *)((BYTE *)info + info->NextEntryOffset);
31    }
32
33    if (!BeginWatch(ctx)) {
34        fprintf(stderr, "Failed to re-arm watch: %lu\n", GetLastError());
35    }
36}

Re-issuing ReadDirectoryChangesW at the end is what keeps the watcher alive.

The Alertable Wait Is Mandatory

This is the detail most examples skip: a completion routine does not run unless the thread enters an alertable wait state.

A minimal loop looks like this:

c
1int main(void) {
2    WatchContext ctx;
3    ZeroMemory(&ctx, sizeof(ctx));
4
5    ctx.directory = CreateFileW(
6        L"C:\\watched",
7        FILE_LIST_DIRECTORY,
8        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
9        NULL,
10        OPEN_EXISTING,
11        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
12        NULL
13    );
14
15    if (ctx.directory == INVALID_HANDLE_VALUE) {
16        fprintf(stderr, "CreateFileW failed: %lu\n", GetLastError());
17        return 1;
18    }
19
20    if (!BeginWatch(&ctx)) {
21        fprintf(stderr, "BeginWatch failed: %lu\n", GetLastError());
22        return 1;
23    }
24
25    for (;;) {
26        SleepEx(INFINITE, TRUE);
27    }
28}

Without SleepEx or another alertable wait API, the callback never fires even though the directory changes are happening.

Common Pitfalls

The most common mistake is using a stack-allocated buffer or OVERLAPPED structure that goes out of scope while the I/O is still pending. The buffer and OVERLAPPED must live until completion.

Another frequent issue is forgetting FILE_FLAG_OVERLAPPED when opening the directory. Without it, you are not using asynchronous I/O correctly.

Developers also often forget the alertable wait requirement. A completion routine is not like a new worker thread. It runs only when the original thread enters an alertable wait state.

Buffer overflow is another practical problem. If the buffer is too small, Windows can report ERROR_NOTIFY_ENUM_DIR, and you may need to rescan the directory to rebuild state.

Finally, do not perform heavy work directly inside the completion routine. Parse the notifications, queue lighter work elsewhere if needed, and re-arm the watch promptly.

Summary

  • Open the directory with FILE_FLAG_BACKUP_SEMANTICS and FILE_FLAG_OVERLAPPED.
  • Keep the buffer and OVERLAPPED alive until the asynchronous request completes.
  • Use a completion routine only if the watching thread enters an alertable wait such as SleepEx(INFINITE, TRUE).
  • Re-issue ReadDirectoryChangesW after handling each notification batch.
  • Plan for overflow and keep the completion routine lightweight.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.