One thing to keep in mind is that this mode of programming is actually not the most performant way to handle many problems. It is simply the most expedient way to manage I/O and spread trivial things across many cores in large, complex codebases. You can typically retrofit an existing code pile to be async-capable without a whole lot of suffering.
If you are trying to go as fast as possible, then async is not what you want at all. Consider that the minimum grain of a Task.Delay is 1 millisecond. A millisecond is quite a brutish unit when working with a CPU that understands nanoseconds. This isn't even a reliable 1 millisecond delay either... There is a shitload of context switching and other barbarism that occurs when you employ async/await.
If you are seeking millions of serialized items per second, you usually just want 1 core to do that for you. Any degree of context switching (which is what async/await does for a living) is going to chop your serialized throughput numbers substantially. You want to batch things up and process them in chunks on a single thread that never gets a chance to yield to the OS. Only problem with this optimization is that it usually means you rewrite from zero, unless you planned for this kind of thing in advance.