Repository navigation
SocketModeClient.close() leaks current_session_runner thread (built-in client) #1873
Description
Reproducible in:
$ pip freeze | grep slack
slack_sdk==3.41.0
$ python --version
Python 3.11.2
$ uname -srv
Linux 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17)Also reproducible on main (commit at the time of filing) — the relevant lines in slack_sdk/socket_mode/builtin/client.py are unchanged from the v3.41.0 release.
The Slack SDK version
slack_sdk==3.41.0
Python runtime version
Python 3.11.2
OS info
Linux 6.12.57+deb12-amd64
Steps to reproduce:
The built-in SocketModeClient starts three IntervalRunner threads in __init__:
current_session_runner— interval 0.1 s (line ~126)current_app_monitor— intervalping_interval(default 5 s) (line ~129)message_processor— interval 0.001 s (line ~134)
close() shuts down current_app_monitor, message_processor, and message_workers, but does not call current_session_runner.shutdown() (lines ~224–232). So every SocketModeClient instance leaks one IntervalRunner thread running a 100 ms loop.
Minimal reproducer (no network, no real token needed):
import threading, time
from slack_sdk.socket_mode.builtin.client import SocketModeClient
c = SocketModeClient(app_token="xapp-fake-not-used")
c.close()
time.sleep(0.5)
print("current_session_runner.is_alive():", c.current_session_runner.is_alive())
print("current_app_monitor.is_alive(): ", c.current_app_monitor.is_alive())
print("message_processor.is_alive(): ", c.message_processor.is_alive())
# Repeat to show the linear leak
for _ in range(5):
c2 = SocketModeClient(app_token="xapp-fake-not-used")
c2.close()
time.sleep(0.5)
print("active_count after 5 more cycles:", threading.active_count())Expected result:
After close(), all three runner threads exit and threading.active_count() returns to its baseline.
Actual result:
current_session_runner.is_alive(): True
current_app_monitor.is_alive(): False
message_processor.is_alive(): False
active_count after 5 more cycles: 7
Each init/close cycle leaks exactly one thread (the current_session_runner). In a long-running watcher that reconnects occasionally — e.g. on transient network blips or when the caller recreates the client in response to is_connected() == False — the leaked threads accumulate. Each one is a 100 ms loop, and combined with the still-running message_processor's 1 ms loop on the live instance, CPU usage climbs noticeably (in our case to 100 % of one core, with 26 threads — 19 in `clock_nanosleep` — after ~2 days of operation against a single workspace).
Proposed fix
One additional shutdown call in `close()`:
```python
def close(self):
self.closed = True
self.auto_reconnect_enabled = False
self.disconnect()
if self.current_session_runner.is_alive(): # <-- added
self.current_session_runner.shutdown() # <-- added
if self.current_app_monitor.is_alive():
self.current_app_monitor.shutdown()
if self.message_processor.is_alive():
self.message_processor.shutdown()
self.message_workers.shutdown()
```
Happy to send a PR if it would help.
Thanks for maintaining the SDK!
Activity
github-actions commentedon Jun 15, 2026
👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized.
github-actions commentedon Jun 29, 2026
As this issue has been inactive for more than one month, we will be closing it. Thank you to all the participants! If you would like to raise a related issue, please create a new issue which includes your specific details and references this issue number.
WilliamBergamin commentedon Sep 3, 2026
Hello @animaartificialis thanks for bringing this up 💯 I was able to reproduce this behavior locally 🥇
Our bad it seems like our automations closed this issue before we were able to address it
fix(socket_mode): shut down current_session_runner in built-in close()