OK, after delving into the asio::ssl code, I finally managed to do this by closing the underlying socket when async_read_some() finishes with an error. The crux of this problem is that, when the client sends a "close alert" to the TLS server, the server subsequently closes the connection without sending back a "close alert" (as permitted by TLS standard), causing async_read_some() to catch the error and fail. Meanwhile, async_shutdown() is still blindly waiting for the server to respond, which never happens (ASIO always assumes that the server will respond with a "close alert": https://github.com/boostorg/asio/blob/886839c/include/boost/asio/ssl/detail/...), unless we propagate the error by closing the socket. IMO, asio should probably "remember" the error state of the stream and make all the pending operations fail. At least this avoids some memory leak.