TL DR
Your assumption is true: they are designed for the same, but implemented in different ways.
discussion
Things in the timer module such as timer:send_after/2,3 work through gen_server, which defines this as a service. Like any other service, this one can be overloaded if you assign it a really huge number of tasks (timers for tracking).
erlang:send_after/3,4 , on the other hand, is a BIF implemented as NIF. If you have a ton of timers, this is definitely the way to go. In most programs, you will not notice the difference.
There is actually a note in the Erlang Performance Guide :
3.1 Timer module
Creating timers using erlang: send_after / 3 and erlang: start_timer / 3 is much more efficient than using timers provided by the timer module in STDLIB. The timer module uses a separate process to manage timers. This process can easily be overloaded if many processes often create and cancel timers (especially when using the SMP emulator).
Functions in the timer module that do not control timers (such as timer: tc / 3 or timer: sleep / 1) do not invoke the timer server process and are therefore harmless.
Workaround
A workaround to increase the efficiency of BIF without limiting the same node is to have your own process that does nothing but wait for a message to be sent to another node:
-module(foo_forward). -export([send_after/3, cancel/1]). % Obviously this is an example only. You would want to write this to % be compliant with proc_lib, write a proper init/N and integrate with % OTP. Note that this snippet is missing the OTP service functions. start() -> spawn(fun() -> loop(self(), [], none) end). send_after(Time, Dest, Message) -> erlang:send_after(Time, self(), {forward, Dest, Message}). loop(Parent, Debug, State) -> receive {forward, Dest, Message} -> Dest ! Message, loop(Parent, Debug, State); {system, From, Request} -> sys:handle_msg(Request, From, Parent, ?MODULE, Debug, State); Unexpected -> ok = log(warning, "Received message: ~tp", [Unexpected]), loop(Parent, Debug, State) end.
The above example is a bit superficial, but hopefully it reflects the gist. It should be possible to get the efficiency of BIF erlang:send_after/3,4 but still manage to send messages through the nodes, and also give you the opportunity to cancel the message using erlang:cancel_timer/1
But why?
The riddle (and error) is that erlang:send_after/3,4 does not want to work between nodes. The above example looks somewhat strange, since the first assignment for P was Pid <10101.10.0> , but the <10101.10.0> call was reported as <10585.83.0> - clearly not the same.
At the moment I do not know why erlang:send_after/3,4 does not work, but I can say with confidence that the mechanism of work between them is not the same. I will look at this, but I imagine that the BIF version actually does some fun things at runtime to increase efficiency and, as a result, signals the target process by directly updating its mailbox, instead of actually sending an Erlang message to more High Erlang. Erlang level.
Maybe itβs good that we have both, but this should be clearly noted in the documents, and this obviously is not (I just checked).