Summary
com.stytch.java.consumer.StytchClient (and B2BClient) holds resources that need explicit cleanup but exposes no way to release them. Applications that hold a single StytchClient for the entire process lifetime are fine — anything that creates a StytchClient per tenant, per test, or per config reload leaks until the JVM exits.
What's held
From the bytecode of StytchClient in sdk-11.0.0:
private final kotlinx.coroutines.CoroutineScope coroutineScope;
private final com.stytch.java.http.HttpClient httpClient;
private final com.stytch.java.http.HttpClient fraudHttpClient;
Each HttpClient wraps an okhttp3.OkHttpClient, which owns a Dispatcher backed by an ExecutorService plus a ConnectionPool with idle-eviction threads. The coroutineScope is created from a SupervisorJob and never cancelled.
Because StytchClient doesn't implement Closeable / AutoCloseable and none of the internal fields are exposed, callers can't release any of it.
Why it matters
- Tests. A test that constructs a real
StytchClient (rather than a mock) leaks an OkHttpClient per test method. Across a suite the dispatcher threads pile up until OkHttp's idle eviction runs.
- Per-tenant clients. Multi-tenant servers that scope a
StytchClient per tenant keep every retired tenant's clients alive.
- Graceful shutdown. Frameworks with structured shutdown can't release the SDK's resources before the rest of the application has stopped, which is awkward when ordering matters.
Proposed fix
StytchClient (and B2BClient) implements AutoCloseable:
class StytchClient(...) : AutoCloseable {
override fun close() {
httpClient.client.dispatcher.executorService.shutdown()
httpClient.client.connectionPool.evictAll()
fraudHttpClient.client.dispatcher.executorService.shutdown()
fraudHttpClient.client.connectionPool.evictAll()
coroutineScope.cancel()
}
}
Callers can then use StytchClient(...).use { ... } for short-lived cases, and DI containers can register a shutdown hook that calls close().
Environment
- SDK:
com.stytch.java:sdk:11.0.0
- JVM: 21
- Kotlin: 2.x
Happy to open a PR if a maintainer signals which direction is preferred (full AutoCloseable versus an explicit shutdown() method).
Summary
com.stytch.java.consumer.StytchClient(andB2BClient) holds resources that need explicit cleanup but exposes no way to release them. Applications that hold a singleStytchClientfor the entire process lifetime are fine — anything that creates aStytchClientper tenant, per test, or per config reload leaks until the JVM exits.What's held
From the bytecode of
StytchClientinsdk-11.0.0:Each
HttpClientwraps anokhttp3.OkHttpClient, which owns aDispatcherbacked by anExecutorServiceplus aConnectionPoolwith idle-eviction threads. ThecoroutineScopeis created from aSupervisorJoband never cancelled.Because
StytchClientdoesn't implementCloseable/AutoCloseableand none of the internal fields are exposed, callers can't release any of it.Why it matters
StytchClient(rather than a mock) leaks anOkHttpClientper test method. Across a suite the dispatcher threads pile up until OkHttp's idle eviction runs.StytchClientper tenant keep every retired tenant's clients alive.Proposed fix
StytchClient(andB2BClient) implementsAutoCloseable:Callers can then use
StytchClient(...).use { ... }for short-lived cases, and DI containers can register a shutdown hook that callsclose().Environment
com.stytch.java:sdk:11.0.0Happy to open a PR if a maintainer signals which direction is preferred (full
AutoCloseableversus an explicitshutdown()method).