Looking at the log you provided in the GitHub issue, the internal per-thread dispatcher (which is a `ref` object) is what causes the leak, as `.threadvar`'s aren't destroyed on thread exit.
* * * Not directly related to the leak, but the way you're using `cleanupThread` doesn't guarantee that the list with potential cycle roots is empty on thread termination. Consider the following: type Cycle = ref object x: Cycle proc run() {.thread.} = var x = Cycle() x.x = x # create a cycle discard x # make sure `x` is not moved cleanupThread() Run In the case shown above, the cycle collector is run _before_ `x` goes out of scope. When `x` goes out of scope, the referenced cell will be registered as a potential cycle root (which allocates memory for ORC's root list), and you'll thus get a memory leak. To make sure that `cleanupThread` is called _after_ the thread's procedure exits but _before_ thread destruction, you can use `system.onThreadDestruction`, like so: proc run() {.thread.} = onThreadDestruction(cleanupThread) # `cleanupThread` is invoked even if `run` exits due to an exception or early return ... Run Of course, it'd be better if `Thread` cleans up after itself instead.