Note that more generally, if you don’t want to wait for a long-running process to terminate, i.e. during an HTTP request, you can also easily spawn a new process. To be clear and to reiterate some info in this thread, these processes are very cheap: they are part of the Erlang Runtime System (ERTS), and are not OS processes, and are unrelated to the threads/cores available on the system. You can spawn a new process using one of the methods documented here. A simple example would be:
def some_route_handler(conn, _params) do
spawn(fn -> long_running_process() end)
send_resp(conn, 202, "Accepted")
Even though a synchronous operation won’t block execution for other users, it will of course for the current user waiting for that process, but spawning another process in this way allows the current process to continue execution without waiting.
I would consider sending email as a long running process, because you never know how long this might take, and for many situations (i.e. sending a welcome email) there isn’t much I would want to do if the mail didn’t send. I might implement retry logic, but that could go in the spawned process as well. If it ultimately does fail, I probably don’t want to show a cryptic and somewhat distressing “Failed to send welcome email” message to my user, so there is no point in making them wait. If you can show a useful error message, and determine it’s worth the tradeoff of making users wait longer in exchange for being able to show that message when there is a failure, then you can make it synchronous.
I use a mailing library called Bamboo that encourages asynchronous behavior with its
deliver_later function, but it looks like with Swoosh you just need to spawn a process yourself if you want that. Conversely, for synchronous behavior Bamboo provides
deliver_now, while that’s the default with Swoosh. I’ve mostly gone the asynchronous route, but hopefully this gives you some information on how/when to implement synchronous/asynchronous behavior.