# Async UDP socket usage

**URL:** <https://discourse.julialang.org/t/async-udp-socket-usage/124401>\
**Category:** General Usage\
**Tags:** question, networking, sockets\
**Created:** [January 3, 2025, 6:56am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401 "2025-01-03T06:56:52Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![mchitre](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mchitre/32/13756_2.png) [@mchitre](https://discourse.julialang.org/u/mchitre)\
**Post date:** [January 3, 2025, 6:56am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/1 "2025-01-03T06:56:52Z")

</div>

We have a problem with async usage of UDP sockets, so I made a MWE to demonstrate it.

We run a simple UDP echo deamon with `socat`:

```julia
socat -v PIPE udp-recvfrom:9809,fork

```

This listens on port 9809 and echoes back to the sender’s UDP port.

This code works:

```julia
using Sockets

sock = UDPSocket()
bind(sock, ip"127.0.0.1", 0)

send(sock, ip"127.0.0.1", 9809, "hello")
@info String(recv(sock))

sleep(1)
close(sock)

```

and prints the echoed packet back.

This code does not work:

```julia
using Sockets

sock = UDPSocket()
bind(sock, ip"127.0.0.1", 0)

@async begin
  @info String(recv(sock))
end
send(sock, ip"127.0.0.1", 9809, "hello")

sleep(1)
close(sock)

```

in the sense that nothing is received back.

Placing small delays in the code makes it work. For example a `sleep(0.001)` inside the `@async` will make the code work.

Depending on the exact timing of when the `send()` and `recv()` are executed, either nothing is received (UDP packet is silently dropped) or an exception `EOFError: read end of file` is thrown. Neither makes sense to me.

So, the question is, should I not expect `send()` and `recv()` using the same socket to work in asynchronous code?

---

<div class="post-metadata">

**Author:** ![AMJ](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/amj/32/214096_2.png) [@AMJ](https://discourse.julialang.org/u/AMJ)\
**Post date:** [January 3, 2025, 7:16am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/2 "2025-01-03T07:16:31Z")

</div>

There is a high chance you are reading your socket buffer without anything being written to it beforehand. You have to use `wait` for `send(sock, ip"127.0.0.1", 9809, "hello")` first.

Another way would be to put your async part in a while loop like this:

```julia
@async begin
   while isopen(sock)
        @info String(recv(sock))
   end
end
```

---

<div class="post-metadata">

**Author:** ![mchitre](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mchitre/32/13756_2.png) [@mchitre](https://discourse.julialang.org/u/mchitre)\
**Post date:** [January 3, 2025, 7:37am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/3 "2025-01-03T07:37:38Z")

</div>

`recv()` is meant to be block for data. From the docs:

```julia
  recv(socket::UDPSocket)

Read a UDP packet from the specified socket, and return
the bytes received. This call blocks.

```

I tested the code snippet with the `while` loop, and it has the same problem.

---

<div class="post-metadata">

**Author:** ![AMJ](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/amj/32/214096_2.png) [@AMJ](https://discourse.julialang.org/u/AMJ)\
**Post date:** [January 3, 2025, 12:32pm UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/4 "2025-01-03T12:32:44Z")

</div>

My bad, I forgot about the blocking part. I think pinning the `recv` function to a separate thread should work as expected.

```julia
sock = UDPSocket()
bind(sock, ip"127.0.0.1", 0)

StableTasks.@spawnat 4 while isopen(sock1)
    recv(sock) |> String |> println
end

for i in 1:5
    send(sock, ip"127.0.0.1", 9809, "hello"); sleep(0.1)
end

```

Just in case if you are not familiar with `StableTasks.@spawnat 4`, it is starting and pining the task to thread 4, assuming you have started julia with 4 threads in this case.

---

<div class="post-metadata">

**Author:** ![mchitre](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mchitre/32/13756_2.png) [@mchitre](https://discourse.julialang.org/u/mchitre)\
**Post date:** [January 6, 2025, 9:28am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/5 "2025-01-06T09:28:14Z")

</div>

Threads works better, but still has the same problem sometimes. In any case, any reason why async on a single thread should not be expected to work? Nothing in the documentation suggests that it should not.

---

<div class="post-metadata">

**Author:** ![mchitre](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mchitre/32/13756_2.png) [@mchitre](https://discourse.julialang.org/u/mchitre)\
**Post date:** [January 6, 2025, 11:40am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/7 "2025-01-06T11:40:13Z")

</div>

The whole point of async is to not block on receive. And it does not. The send works fine. I can even see the transmitted and received UDP packets on a tcpdump. It is the receive that never gets it when it fails.

And it works sometimes depending on timing. So it isn’t a systematic problem with blocking.

---

<div class="post-metadata">

**Author:** ![ettersi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ettersi/32/6829_2.png) [@ettersi](https://discourse.julialang.org/u/ettersi)\
**Post date:** [January 9, 2025, 7:20am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/8 "2025-01-09T07:20:03Z")

</div>

One thing that’s for sure suspicious about your code is that you are starting the `send()` before the `recv()`. (`@async` only schedules a task but doesn’t immediately run it, so the `@async` task and therefore the `recv()` are executed only when the main task yields, which is during the `send()`.) Fixing this order fixes the code, i.e. the following works.

```julia
julia> sock = UDPSocket()
       bind(sock, ip"127.0.0.1", 0)

       # This switches to the task immediately
       yield(@task @info String(recv(sock))) 
       send(sock, ip"127.0.0.1", 9809, "hello")

       sleep(1)
       close(sock)

```

What I don’t understand is why executing the `send()` before the `recv()` consistently fails. Usually, the networking stack caches unreceived datagrams such that the following works just fine:

```julia
julia> sock = UDPSocket()
       bind(sock, ip"127.0.0.1", 0)

       send(sock, ip"127.0.0.1", 9809, "hello")
       @info String(recv(sock))

       close(sock)
[ Info: hello

```

My best guess is that there is some weird interaction between the `send()` and `recv()` which causes the `recv()` to not fire correctly if you flip-flop between the two.

---

<div class="post-metadata">

**Author:** ![ettersi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ettersi/32/6829_2.png) [@ettersi](https://discourse.julialang.org/u/ettersi)\
**Post date:** [January 9, 2025, 7:44am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/9 "2025-01-09T07:44:07Z")

</div>

Hah, I think I know what’s happening! The problem is that `recv()` first checks whether the UDP socket is “active” and only if it’s found to be not active then it registers a callback for when a matching UDP datagram arrives. From [Sockets.jl](https://github.com/JuliaLang/julia/blob/5e9a32e7af2837e677e60543d4a15faa8d3a7297/stdlib/Sockets/src/Sockets.jl#L346-L350):

```julia
if ccall(:uv_is_active, Cint, (Ptr{Cvoid},), sock.handle) == 0
    err = ccall(:uv_udp_recv_start, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
                sock,
                @cfunction(Base.uv_alloc_buf, Cvoid, (Ptr{Cvoid}, Csize_t, Ptr{Cvoid})),
                @cfunction(uv_recvcb, Cvoid, (Ptr{Cvoid}, Cssize_t, Ptr{Cvoid}, Ptr{Cvoid}, Cuint)))

```

The trouble is that sending a datagram also counts as active; therefore, if the `recv()` is executed during the `send()` then the callback is never registered and therefore the `recv()` never resolves.

```julia
julia> sock = UDPSocket()
       bind(sock, ip"127.0.0.1", 0)
       println("Before send: ", is_active(sock))

       @async begin
           println("In async before yield: ", is_active(sock))
           println("In async after yield: ", is_active(sock))
       end
       send(sock, ip"127.0.0.1", 9809, "hello")
       println("After send: ", is_active(sock))

       close(sock)
Before send: 0
In async before yield: 1
In async after yield: 0
After send: 0

```

This also explains why delaying the `recv()` even further by putting a `sleep()` before it fixes the problem: the extra sleep means the task yields once more, and that allows the `send()` to finish before the `recv()` starts.

---

<div class="post-metadata">

**Author:** ![ettersi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ettersi/32/6829_2.png) [@ettersi](https://discourse.julialang.org/u/ettersi)\
**Post date:** [January 9, 2025, 7:55am UTC](https://discourse.julialang.org/t/async-udp-socket-usage/124401/10 "2025-01-09T07:55:22Z")

</div>

Opened an issue on GitHub: [send() / recv() on UDPSocket is not async-safe · Issue #57001 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/issues/57001)
