I'm not sure when the "blocking_pipes (windows only)" option was added to PHP, but users of this function should be fully aware that there is no such thing as a non-blocking pipe in PHP on Windows and that the "blocking_pipes" option does NOT function like you might expect. Passing "blocking_pipes" => false does NOT mean non-blocking pipes.
PHP uses anonymous pipes to start processes on Windows. The Windows CreatePipe() function does not directly support overlapped I/O (aka asynchronous), which is typically how async/non-blocking I/O happens on Windows. SetNamedPipeHandleState() has an option called PIPE_NOWAIT but Microsoft has long discouraged the use of that option. PHP does not use PIPE_NOWAIT anywhere in the source code tree. PHP FastCGI startup code is the only place within the PHP source code that uses overlapped I/O (and also the only place that calls SetNamedPipeHandleState() with PIPE_WAIT). Further, stream_set_blocking() on Windows is only implemented for sockets - not file handles or pipes. That is, calling stream_set_blocking() on pipe handles returned by proc_open() will actually do nothing on Windows. We can derive from these facts that PHP does not have a non-blocking implementation for pipes on Windows and will therefore block/deadlock when using proc_open().
PHP's pipe read implementation on Windows uses PeekNamedPipe() by polling on the pipe until there is some data available to read OR until ~32 seconds (3200000 * 10 microseconds of sleep) have passed before giving up, whichever comes first. The "blocking_pipes" option, when set to true, changes that behavior to wait indefinitely (i.e. always block) until there is data on the pipe. It's better to view the "blocking_pipes" option as a "possibly 32 second busy wait" timeout (false - the default value) vs. no timeout (true). In either case, the boolean value for this option effectively blocks...it just happens to block a lot longer when set to true.
The undocumented string "socket" descriptor type can be passed to proc_open() and PHP will start a temporary TCP/IP server and generate a pre-connected TCP/IP socket pair for the pipe and pass one socket to the target process and return the other as the associated pipe. However, passing a socket handle for stdout/stderr on Windows causes the last chunk(s) of output to occasionally get lost and not be delivered to the receiving end. This is actually a known bug in Windows itself and Microsoft's response at one point was that CreateProcess() only officially supports anonymous pipes and file handles for the standard handles (i.e. not named pipes or socket handles) and that other handle types will produce "undefined behavior." For sockets, it will "sometimes work fine and sometimes truncate the output." The "socket" descriptor type also introduces a race condition that is probably a security vulnerability in proc_open() where another process can successfully connect to the server side BEFORE the original process connects to the socket to create the socket pair. This allows a rogue application to send malformed data to a process, which could trigger anything from privilege escalation to SQL injection depending on what the application does with the information on stdout/stderr.
To get true non-blocking I/O in PHP for Windows for standard process handles (i.e. stdin, stdout, stderr) without obscure bugs cropping up, the only currently working option is to use an intermediary process that uses TCP/IP blocking sockets to route data to blocking standard handles via multithreading (i.e. start three threads to route data between the TCP/IP socket and the standard HANDLE and use a temporary secret to prevent race conditions when establishing the TCP/IP socket handles). For those who lost count: That's one extra process, up to four extra threads, and up to four TCP/IP sockets just to get functionally correct non-blocking I/O for proc_open() on Windows. If you vomited a little bit at that idea/concept, well, people actually do this! Feel free to vomit some more.