Skip to content

Add websocket_max_workers and run async callbacks directly on the loop#3826

Merged
T4rk1n merged 6 commits into
devfrom
feat/per-websocket-threadpool
Jun 30, 2026
Merged

Add websocket_max_workers and run async callbacks directly on the loop#3826
T4rk1n merged 6 commits into
devfrom
feat/per-websocket-threadpool

Conversation

@T4rk1n

@T4rk1n T4rk1n commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@camdecoster camdecoster self-assigned this Jun 25, 2026

@camdecoster camdecoster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any protections in place for the case of a server getting hammered and increasing the thread count continuously (when an app fires four sync callbacks right away)?

Comment thread dash/backends/_fastapi.py Outdated
pending_get_props: Dict[str, queue.Queue] = {}
# Track pending get_props requests. Values are queue.Queue (threadpool /
# sync path) or asyncio.Future (event-loop / async path).
pending_get_props: Dict[str, Any] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this type be tightened up a bit?

Comment thread dash/backends/_fastapi.py

# The connection's event loop, used to dispatch async callbacks as
# tasks and to resolve get_props futures.
loop = asyncio.get_running_loop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of enabling debug mode for the loop here (and in a similar place for Quart)? You could use the existing debug config variable to turn this on. This way, users would get warning when a callback is executing slowly. You could also add a way for users to configure loop.slow_callback_duration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, maybe as a follow up.

Comment thread dash/backends/_fastapi.py

# The connection's event loop, used to dispatch async callbacks as
# tasks and to resolve get_props futures.
loop = asyncio.get_running_loop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option (maybe in tandem) would be to add a watchdog thread outside of the pool that could keep an eye out for bottlenecked callbacks.

Comment thread dash/backends/_fastapi.py Outdated
Comment on lines +741 to +743
executor = self.create_callback_executor(
getattr(dash_app, "_websocket_max_workers", 4)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of moving this inside the try below (along with the assignment to sender_task? If an error occurs before the try, the executor is never shut down.

Comment thread dash/backends/_fastapi.py Outdated
pending_callbacks: Dict[str, concurrent.futures.Future] = {}
# Create a per-connection thread pool executor so that long-lived
# callbacks on one connection cannot starve worker threads for others.
# pylint: disable=protected-access

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete these stale pylint comments.

@sonarqubecloud

Copy link
Copy Markdown

@camdecoster camdecoster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems good overall. I think the debug addition would be good to add later, but this addresses the underlying issue.

@@ -209,7 +215,7 @@ def get_callback_executor(
return self._callback_executor

def shutdown_executor(self, wait: bool = True) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function needed anymore?

Comment thread dash/backends/_fastapi.py
# Create WebSocket callback instance
# Async callbacks (incl. persistent ones) run as tasks on
# the event loop; sync callbacks go to the threadpool.
# pylint: disable=protected-access

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can still remove these stale pylint comments.

@T4rk1n T4rk1n changed the title Add per-connection threadpool websocket callback executor. Add websocket_max_workers and run async callbacks directly on the loop Jun 30, 2026
@T4rk1n T4rk1n merged commit 4699277 into dev Jun 30, 2026
31 checks passed
@T4rk1n T4rk1n deleted the feat/per-websocket-threadpool branch June 30, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebSocket callback concurrency is bounded by an unconfigurable thread pool

2 participants