I’m not an elixir user, nor a coroutine expert but i’m gonna give it a shot.
Coroutines are a simple compiler trick, you transform a method containing suspension points into a huge switch case where each case is the code between two suspension points. There is a whole bunch of machinery above that to correctly pass contexts around but you get the idea.
This allows us to create thousands of coroutines whithout problem, their cost is a few bytes of memory for the internal objects they allocate. If you are interested I suggest you take a look at what the Kotlin/JS compiler produces when dealing with coroutines, it’s really interesting and it helps you understand the underlying machinery a bit better.
Threads are what coroutines are dispatched to, they are simple workers that execute code until reaching the next suspension point. This architecture is light in term of memory, and in term of scheduling for the OS, you can execute a lot of coroutines on the same threads easily, allowing you to decrease the number of threads your app needs, reducing the number of context-switch.
Another good thing with coroutines is that it is way easier to correctly do concurrency (with things like the actor paradigm
The main cost of coroutines in my experience is dispatching latency and garbage collection pressure. As they are made of objects, suspension can become costly if you do a lot of them. In my case it was a networking library doing thousands of suspension per second, leading to a bit of garbage collection pressure and a bit of dispatching latency (nothing catastrophic but still).
To come back to your initial question, yes coroutines spawn way faster than threads, but if your worker pool is busy, you can end up with a bit of dispatching latency. I can’t compare with elixir processes as I don’t know them, but if they are actual processes, there is a good chance coroutines spawn faster too.
For me coroutines are light, scalable, fast, and I can’t recommend them enough