The fundamental problem isn't async code, it's buffering. Async-await patterns simply shift scheduling from the system (kernel or low-level runtime) into the application, but that's it. Languages like Python and JavaScript provide broken async-await APIs that do too much hidden buffering, including unbounded buffering. But that's a stupid design decision on their part, a reflection of poor interface design arguably stemming from a) bolting async I/O onto pre-existing architectures and runtimes that can't accommodate it well, and b) the implementers not having sufficient real-world experience designing and implementing such systems.
But even in threaded code you can have buffering problems. For example, Linux' default socket buffer size sysctls are arguably far too large, optimized for high throughput of a few connections (i.e. benchmarks), not for good responsiveness. That makes it too easy for any massively concurrent service daemon, threaded or not, to flood the outbound network queues.
The answer for any I/O model is to never, ever implement an in-memory input or output sink with an unlimited size, whether for buffering data, connections, or any other resource (e.g. stack frames if an event loop recursively reenters itself). That solves most of the problems right there. The remaining problems are more nuanced, and may require reducing buffer sizes as much as possible (think single-digit Kb instead of Mb[1]), and considering how various resource buffers and APIs interact (additively, multiplicatively) to create hidden buffer bloat.
[1] Especially if you're not doing any heavy processing of the buffer contents, such as audio-video processing where you may need moderately larger buffer sizes to benefit from pipelined and SIMD processing.