Summary
When a ConfigurationError is raised during startup, the bot logs the error but the process never exits. It sits idle at 0% CPU indefinitely. sys.exit(1) runs, but interpreter finalization blocks forever waiting on the aiosqlite connection pool's non-daemon threads, which are never shut down on this code path.
This is worse than a plain crash, because process supervisors see a live PID and consider the service healthy. launchd reports state = running with last exit code = (never exited), so KeepAlive never fires. The same applies to systemd Restart=. The bot appears to be up while being permanently dead.
Environment
- claude-code-telegram v1.6.0 (installed via
uv tool install)
- Python 3.12.13
- aiosqlite 0.21.0
- macOS 15 (Darwin 25.5.0), arm64
Reproduction
- Configure
.env with ALLOWED_USERS empty, ENABLE_TOKEN_AUTH=false, and DEVELOPMENT_MODE=false.
- Run
claude-telegram-bot.
- The log ends with:
{"event": "Storage system initialized", "logger": "src.storage.facade", "level": "info"}
{"error": "No authentication providers configured", "event": "Configuration error", "logger": "src.main", "level": "error"}
- The process stays alive forever. It has to be killed manually.
Cause
In src/main.py, create_application() initializes storage before validating auth providers:
- line 103-104:
storage = Storage(config.database_url) then await storage.initialize(), which starts a 5 connection aiosqlite pool
- line 125-126:
elif not providers: raise ConfigurationError("No authentication providers configured")
The exception propagates out of create_application() to the handler in main() (line 395), which calls sys.exit(1). The only call to await storage.close() lives in run_application()'s shutdown block (line 362), which is never reached, because the failure happened before run_application() was ever called. The pool is left running.
aiosqlite.Connection subclasses threading.Thread and calls super().__init__() without setting daemon, so those 5 connections are non-daemon threads parked on self._tx.get(). They only stop when close() posts _STOP_RUNNING_SENTINEL. Non-daemon threads are joined during interpreter shutdown, so Py_FinalizeEx waits on them forever.
sample of the stuck process, main thread:
Py_Exit (in python3.12) + 20
Py_FinalizeEx (in python3.12) + 72
wait_for_thread_shutdown (in python3.12) + 104
lock_PyThread_acquire_lock (in python3.12) + 56
PyThread_acquire_lock_timed (in python3.12) + 432
_pthread_cond_wait (in libsystem_pthread.dylib) + 980
and five identical worker threads:
thread_run (in python3.12) + 148
_queue_SimpleQueue_get (in python3.12) + 220
_queue_SimpleQueue_get_impl (in python3.12) + 232
PyThread_acquire_lock_timed (in python3.12) + 432
_pthread_cond_wait (in libsystem_pthread.dylib) + 980
For contrast, an invalid TELEGRAM_BOT_TOKEN exits cleanly. InvalidToken is raised from bot.initialize() inside run_application(), where the shutdown block does run. It is specifically failures raised between storage.initialize() and run_application() that wedge.
Impact
Anyone running this under launchd, systemd, Docker with a restart policy, or any other supervisor gets a silently dead bot after a config mistake, with no restart and no crash to alert on. It also affects the two ENABLE_PROJECT_THREADS errors at lines 239 and 259, and any other exception escaping create_application() after storage init.
Suggested fix
Close storage when application construction fails, for example:
async def create_application(config: Settings) -> Dict[str, Any]:
storage = Storage(config.database_url)
await storage.initialize()
try:
# ... existing body ...
return {...}
except BaseException:
await storage.close()
raise
Validating auth providers before initializing storage would also avoid this particular instance, but the try/except covers every failure between pool startup and run_application(), which seems more robust. Marking the aiosqlite threads as daemon would mask the hang but leave connections unclosed, so it looks like the weaker option.
Happy to open a PR if that would help.
Summary
When a
ConfigurationErroris raised during startup, the bot logs the error but the process never exits. It sits idle at 0% CPU indefinitely.sys.exit(1)runs, but interpreter finalization blocks forever waiting on the aiosqlite connection pool's non-daemon threads, which are never shut down on this code path.This is worse than a plain crash, because process supervisors see a live PID and consider the service healthy.
launchdreportsstate = runningwithlast exit code = (never exited), soKeepAlivenever fires. The same applies to systemdRestart=. The bot appears to be up while being permanently dead.Environment
uv tool install)Reproduction
.envwithALLOWED_USERSempty,ENABLE_TOKEN_AUTH=false, andDEVELOPMENT_MODE=false.claude-telegram-bot.{"event": "Storage system initialized", "logger": "src.storage.facade", "level": "info"} {"error": "No authentication providers configured", "event": "Configuration error", "logger": "src.main", "level": "error"}Cause
In
src/main.py,create_application()initializes storage before validating auth providers:storage = Storage(config.database_url)thenawait storage.initialize(), which starts a 5 connection aiosqlite poolelif not providers: raise ConfigurationError("No authentication providers configured")The exception propagates out of
create_application()to the handler inmain()(line 395), which callssys.exit(1). The only call toawait storage.close()lives inrun_application()'s shutdown block (line 362), which is never reached, because the failure happened beforerun_application()was ever called. The pool is left running.aiosqlite.Connectionsubclassesthreading.Threadand callssuper().__init__()without settingdaemon, so those 5 connections are non-daemon threads parked onself._tx.get(). They only stop whenclose()posts_STOP_RUNNING_SENTINEL. Non-daemon threads are joined during interpreter shutdown, soPy_FinalizeExwaits on them forever.sampleof the stuck process, main thread:and five identical worker threads:
For contrast, an invalid
TELEGRAM_BOT_TOKENexits cleanly.InvalidTokenis raised frombot.initialize()insiderun_application(), where the shutdown block does run. It is specifically failures raised betweenstorage.initialize()andrun_application()that wedge.Impact
Anyone running this under launchd, systemd, Docker with a restart policy, or any other supervisor gets a silently dead bot after a config mistake, with no restart and no crash to alert on. It also affects the two
ENABLE_PROJECT_THREADSerrors at lines 239 and 259, and any other exception escapingcreate_application()after storage init.Suggested fix
Close storage when application construction fails, for example:
Validating auth providers before initializing storage would also avoid this particular instance, but the
try/exceptcovers every failure between pool startup andrun_application(), which seems more robust. Marking the aiosqlite threads as daemon would mask the hang but leave connections unclosed, so it looks like the weaker option.Happy to open a PR if that would help.