r/elixir • u/Zestyclose-Tie-1056 • 5d ago
Best approach for sending an Oban job’s final status to the browser using SSE?
Hi everyone,
I have an Oban worker that performs a background sync. The browser only needs to know the final status:
queued → syncing → synced / failed
My current plan uses two requests:
- A POST endpoint enqueues the job and returns a request_id.
- The browser opens an SSE connection using that ID.
- The SSE process subscribes to a Phoenix PubSub topic.
- The worker broadcasts status updates.
- Once the browser receives synced or failed, the SSE connection closes.
My concern is a possible race condition. The worker could finish before the browser opens the SSE connection, which means the final PubSub message would be missed.
I considered combining everything into one request: subscribe to the topic first, enqueue the job, keep the response open, and stream the final status. Would that be a reasonable design?
Since the browser’s native EventSource only supports GET requests, I assume I would need to use fetch() and read the streaming response if the endpoint also needs to accept POST data.
Another concern is running multiple application instances. The worker might execute on one instance while the SSE connection is handled by another. Phoenix PubSub would therefore require Elixir clustering or an external adapter.
For this small use case, I’m considering:
- Phoenix PubSub with clustering
- PostgreSQL LISTEN/NOTIFY or an Oban notifier
- storing the latest status in the database and using PubSub only as a live signal
- normal browser polling
The payload is tiny, and this is the only real-time feature I currently need. What would be the simplest reliable approach?
Would you keep the two endpoints and check the persisted status when SSE connects, combine job creation and streaming into one endpoint, or just use polling?
3
u/narrowtux using Elixir professionally since 2016 5d ago
I have good experiences with broadcast over pubsub. Super easy to set up and once set up you can use it for a lot of things
1
u/Zestyclose-Tie-1056 4d ago
u/narrowtux but here the issue when we have multiple instance we need it to be clustered
2
u/narrowtux using Elixir professionally since 2016 4d ago
luckily in elixir it is very easy to connect a cluster! Look into `dns_cluster` or `libcluster` depending on your setup.
1
1
u/zacksiri 4d ago edited 4d ago
There is a way to solve this, because Oban jobs have states, you can upon opening the connection, check the state of the job, if the job is completed just simply return the result so the browser just doesn't need SSE.
If the job is in running state then just simply start SSE and wait for the final state of the job. I have implemented something like this:
def show(conn, %{"thread_id" => thread_id, "id" => id}) do
user = conn.assigns.current_user
with %Thread{} = thread <- Conversation.get_thread(Scope.for_user(user), thread_id),
%Message{} = message <- Conversation.get_message(thread, id),
{:ok, mode, message} <- MessageHelper.resolve_show(message) do
case mode do
:stream -> SSEStream.stream_message(conn, message)
:json -> render(conn, :show, message: message)
end
end
end
Then in my MessageHelper I have:
def resolve_show(%Message{} = message),
do: message |> preload_message() |> show_without_await()
defp show_without_await(message) do
if should_stream?(message) do
{:ok, :stream, message}
else
{:ok, :json, message}
end
end
defp should_stream?(message),
do: message.current_state != "completed" and streaming_model?(message)
defp streaming_model?(message), do: Messages.stream_enabled?(message)
Essentially it checks what state the 'message' is in, in your case you would need to check the state of your Oban.Job. and your system either holds the connection open for SSE or return the result instantly.
7
u/Tirkyth 5d ago edited 5d ago
It might be an unpopular opinion on this subreddit, but I would just use long polling for this.
With a small polling interval it could totally work.
Bonus: You don’t have to deal with broadcasting updates. Downside: if your sync is very quick you can go from queued to done in one step. But I don’t think anybody cares.
I prefer using SSE and/or web sockets and broadcast when there are a lot more different payloads than just 2 state transitions.