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 — interval ping_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!
Reproducible in:
Also reproducible on
main(commit at the time of filing) — the relevant lines inslack_sdk/socket_mode/builtin/client.pyare unchanged from the v3.41.0 release.The Slack SDK version
slack_sdk==3.41.0Python runtime version
Python 3.11.2OS info
Linux 6.12.57+deb12-amd64Steps to reproduce:
The built-in
SocketModeClientstarts threeIntervalRunnerthreads 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 downcurrent_app_monitor,message_processor, andmessage_workers, but does not callcurrent_session_runner.shutdown()(lines ~224–232). So everySocketModeClientinstance leaks oneIntervalRunnerthread running a 100 ms loop.Minimal reproducer (no network, no real token needed):
Expected result:
After
close(), all three runner threads exit andthreading.active_count()returns to its baseline.Actual result:
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 tois_connected() == False— the leaked threads accumulate. Each one is a 100 ms loop, and combined with the still-runningmessage_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!