diff --git a/embassy-net/CHANGELOG.md b/embassy-net/CHANGELOG.md
index 4030e050b..ada34cb75 100644
--- a/embassy-net/CHANGELOG.md
+++ b/embassy-net/CHANGELOG.md
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## Unreleased
+
+- Avoid never resolving `TcpIo::read` when the output buffer is empty.
+
 ## 0.2.1 - 2023-10-31
 
 - Re-add impl_trait_projections
diff --git a/embassy-net/src/tcp.rs b/embassy-net/src/tcp.rs
index b5615cb66..bcd5bb618 100644
--- a/embassy-net/src/tcp.rs
+++ b/embassy-net/src/tcp.rs
@@ -390,6 +390,13 @@ impl<'d> TcpIo<'d> {
             // CAUTION: smoltcp semantics around EOF are different to what you'd expect
             // from posix-like IO, so we have to tweak things here.
             self.with_mut(|s, _| match s.recv_slice(buf) {
+                // Reading into empty buffer
+                Ok(0) if buf.is_empty() => {
+                    // embedded_io_async::Read's contract is to not block if buf is empty. While
+                    // this function is not a direct implementor of the trait method, we still don't
+                    // want our future to never resolve.
+                    Poll::Ready(Ok(0))
+                }
                 // No data ready
                 Ok(0) => {
                     s.register_recv_waker(cx.waker());