Designing Containerised Apps for Long-Running Operations
Purpose
Use asynchronous execution when an operation may block the application event loop and interrupt the video stream.
Overview
Containerised applications may experience video-streaming interruptions during long-running synchronous operations. Users may see a white screen, a reconnect spinner, or a temporary loss of the application display while the operation is running.
This can occur during outbound API calls, model inference, complex database queries, file processing, or other tasks that take several seconds to complete.
Why this happens
Many application frameworks process requests and events through a single-threaded event loop. If that loop is occupied by a synchronous operation, the application cannot respond promptly to the keepalive messages required to maintain the video-streaming connection.
When the connection is unresponsive beyond the streaming service's reconnect threshold, it may be treated as disconnected and a reconnect cycle can begin. The display often recovers when the operation completes and the event loop becomes available again, but the interruption creates a poor user experience.
Important: The exact timeout depends on the application stack, network conditions, and streaming configuration. Do not design close to the observed limit. Treat operations expected to take more than a few seconds as candidates for asynchronous execution.
What does not solve the problem
Browser-side techniques such as setInterval, requestAnimationFrame, loading overlays, or additional DOM updates do not resolve a blocked server-side event loop. They may improve feedback when the application remains responsive, but they cannot keep the streaming connection alive if the server cannot process events.
Recommended approach
Move blocking work away from the main application event loop. Use an asynchronous execution model that allows the application to remain responsive while the operation runs in the background.
- Return control to the event loop as soon as work is submitted.
- Provide clear progress or "working" feedback to the user.
- Update the interface when the operation completes.
- Handle failures, cancellation, and timeouts explicitly.
- Prevent duplicate submissions while an operation is already running.
R Shiny example
The following example uses future and promises to run a long-running operation outside the main event loop.
library(future)
library(promises)
plan(multisession)
observeEvent(input$run_query, {
future_promise({
long_running_operation(input$params)
}) %...>%
(function(result) {
rv$data <- result
showNotification("Operation complete!")
}) %...!%
(function(error) {
showNotification(
paste("The operation failed:", conditionMessage(error)),
type = "error"
)
})
NULL
})
The background operation should contain only the work that needs to run asynchronously. UI updates and session-specific changes should be performed in the completion and error handlers, after control has returned to the application event loop.
Implementation checklist
- Identify API calls, database queries, model execution, file processing, and other operations that may block the event loop.
- Use an asynchronous worker, task queue, or background process for operations that may run for several seconds.
- Return an immediate acknowledgement or job identifier to the client.
- Display progress, waiting, or completion status in the user interface.
- Implement error handling, timeouts, retries, and cancellation where appropriate.
- Test under realistic network latency and concurrent-user conditions.
Best practices
Use a conservative threshold
Do not wait for an operation to approach the streaming timeout. As a practical rule, use asynchronous execution for any operation expected to take more than approximately six seconds or whose duration is unpredictable.
Handle external services asynchronously
Outbound HTTP requests and other remote calls should be treated as variable-duration operations. Account for connection timeouts, response timeouts, retries, rate limits, and service failures.
Keep workers independent
Pass the minimum required input to the background worker. Avoid relying on mutable session state, open connections, or objects that cannot be safely shared between processes or threads.
Protect resources
Limit the number of concurrent workers and avoid creating an unbounded process or thread pool. Apply back-pressure or queueing when users can submit work faster than it can be processed.
Provide useful feedback
Tell users when work has started, whether it is still running, and when it has completed. A responsive status message is preferable to leaving the user with a blank or apparently frozen display.
Consider browser-side requests carefully
For applications using Python or JavaScript, browser-side fetch() calls can prevent the application server from being blocked by a remote request. The server must still enforce authentication, authorisation, validation, timeout, and error-handling controls.
Testing considerations
- Test operations that complete quickly and operations that exceed the expected streaming threshold.
- Test slow and unreliable external services.
- Test multiple users running operations concurrently.
- Confirm that the video stream remains available while background work is running.
- Verify that errors and timeouts return the interface to a usable state.
- Confirm that users cannot accidentally submit the same operation multiple times.
Key takeaway
Keep the main application event loop free. Submit long-running work to an asynchronous worker or queue, provide status feedback, and update the interface only when the work completes.