"are we there yet?", said the client to the server

Michael Trimble / Unsplash
Recently needed my front-end to know when some data exports were done. It's a simple csv or xlsx export, and unless the user has heaps of data, it just takes a few seconds. The problem was simple, my server needs to notify the client when the file was ready with no need for a signal to cancel the running process.
Out of a bad habit, my first thought was setting up a status endpoint were I could poll the task's progress, known as short-polling. I've worked with them countless times, and it seems to be the preferred approach in which development teams increase the tech debt they never end up paying off.
There's a couple known solutions for this, and it's just a matter of picking the right one for your specific case. Here, I’ll be yapping a bit about short-polling, long-polling, WebSockets, and Server-Sent Events.
I thought it could be useful for someone to be exposed to these ideas if they haven’t already, and honestly, every time that I’ve done any of these light-hearted write-ups, I ended up having deep and fantastic conversations with other fellow nerds.
Anyway, onward. Short-polling takes a minute to set up, and it's basically asking the job every certain amount of time whether it's finished. The server says whatever, the connection closes and you need to repeat this process until it either fails or succeeds.
It is the developer's version of "Are we there yet?". The issue with this is that most requests are worthless, just burning server cycles and precious bandwidth while opening the door to unnecessary state management bugs.
In my opinion, for long-running jobs that can have a properly spaced interval and the user doesn't require immediate responsiveness, this is perfectly fine.
The other problem is that getting the right interval for short-polling is practically impossible, it is always too slow and the updates are very choppy or too over-eager and you are just bloating the network and annoying your server.
Long-polling on the other hand, it's the same concept but slightly more sophisticated. It works over HTTP and the server keeps the connection open until there's an actual status update, or just times-out after ~30s.
Gracefully handling re-connections and finding the perfect interval with a margin before this happens can be a bit a of a pain, but in my opinion, they are preferred over short-polling unless you have high-frequency data updates.
They keep threads captive which is also not ideal, but this pattern has been serving devs well for a long time.
Now, the real-deal: WebSockets. Despite starting off with an HTTP request, they are their own application-layer protocol running over TCP. Proxies and firewall configs can be a bit of a PIA because of this, but with them you'll get proper bidirectional event streaming.
Both ends send raw bytes whenever they want with no encoding overhead. It's quite speedy. Despite offering a lot, their downsides are add complexity on the infrastructure side and having to handle re-connection yourself.
Unless your really need low-latency bidirectional streaming for something like a collaboration tool or some multiplayer anything, they are an overkill.
Now, for my simple stuff I just ended up going for Server-Sent Events, which is similar to the above but way easier to setup and with no load balancer complaints.
The pattern also works over HTTP with the EventSource API keeping track of the last-event sent and handling re-connection for you. Client opens a connection, and the server streams text events as soon as they happen.
Just configure the Content-Type of your API's response to text/event-stream, and that will flush each yield immediately. The client receives events in real time as your generator produces them rather than waiting for the function to finish.
If you are working on a Python back-end, it's dead simple as most libraries offer out-of-the-box solutions for you. If you are working with FastAPI, take a look at StreamingResponse (ty Starlette!), stream_with_context on Flask, and there’s multiple others.
The way my event works is just plain text: data, event, and id fields. The id field makes re-connection easy: browser sends it back as Last-Event-ID on reconnect, so you can resume from where you left off.
Just got to know your options, and choose the one that best fits your problem. In the end, you could open an envelope with a chainsaw too, but it is prlly not the most efficient way to do it.