Virtual Threads vs async/await
Two answers to the same problem: a thread per request is expensive, so stop blocking an OS thread.
The problem both solve. A blocking call parks an OS thread. OS threads cost ~1MB of stack and a kernel scheduling slot, so a thread-per-request server tops out in the low thousands. Most of those threads are doing nothing but waiting on I/O.
async/await (stackless). The compiler rewrites your function into a state machine; await is a suspension point. Cheap and explicit, but it colours your functions: an async function can only be awaited by another async function, so the asyncness propagates up the entire call graph, and every library needs an async twin. Rust, JavaScript, C#, Python work this way.
Virtual threads (stackful). The runtime gives each task its own growable stack and unmounts it from the carrier thread when it blocks. Existing blocking code just works — no colouring, real stack traces, thread-locals and debuggers behave. Java 21+, Go's goroutines, Erlang's processes work this way. The cost is runtime complexity and, for Java, a long tail of pinning problems: a synchronized block or a native call pins the virtual thread to its carrier and you lose the benefit.
What neither fixes. Concurrency limits. Cheap tasks make it easy to start ten thousand database queries, and your pool has twenty connections. You still need a semaphore, a bounded pool, and backpressure — the bottleneck simply moved somewhere less visible.
Takeaway. Stackless is explicit and viral; stackful is transparent and magic. The interesting question in both is where you put the limit.