bkeepers/dotenv · error · ThreadError
Dotenv.restore is not thread safe. Use `Dotenv.modify { }` t
Error message
Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore this error. What it means
Dotenv.restore replaces the entire process-global ENV with a saved snapshot via ENV.replace, which can corrupt ENV for concurrently running threads. Because the default `safe:` argument is `Thread.current == Thread.main`, dotenv raises ThreadError whenever restore is called from any thread other than the main one. The error message points you to Dotenv.modify { }, which performs the update and restore inside a semaphore, or to explicitly opt in with `safe: true` when you know no other thread depends on ENV.
Source
Thrown at lib/dotenv.rb:85
def save
instrument(:save) do |payload|
@diff = payload[:diff] = Dotenv::Diff.new
end
end
# Restore `ENV` to a given state
#
# @param env [Hash] Hash of keys and values to restore, defaults to the last saved state
# @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error.
def restore(env = @diff&.a, safe: Thread.current == Thread.main)
# No previously saved or provided state to restore
return unless env
diff = Dotenv::Diff.new(b: env)
return unless diff.any?
unless safe
raise ThreadError, <<~EOE.tr("\n", " ")
Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration
of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore
this error.
EOE
end
instrument(:restore, diff: diff) { ENV.replace(env) }
end
# Update `ENV` with the given hash of keys and values
#
# @param env [Hash] Hash of keys and values to set in `ENV`
# @param overwrite [Boolean|:warn] Overwrite existing `ENV` values
def update(env = {}, overwrite: false)
instrument(:update) do |payload|
diff = payload[:diff] = Dotenv::Diff.new do
ENV.update(env.transform_keys(&:to_s)) do |key, old_value, new_value|
# This block is called when a key exists. Return the new value if overwrite is true.
case overwriteView on GitHub (pinned to 34156bf400)
Solutions
- Replace the save/restore pattern with `Dotenv.modify { }`, which sets ENV for the block and restores it under a lock
- If you have verified nothing else reads or writes ENV concurrently, opt in explicitly with `Dotenv.restore(state, safe: true)`
- Restructure so the restore runs on the main thread (e.g. defer it to a queue drained at boot or between jobs)
Example fix
# before
snapshot = Dotenv::Diff.new.a
Thread.new do
Dotenv.restore(snapshot) # raises ThreadError
end
# after
Thread.new do
Dotenv.modify("API_KEY" => "temp") do
# ENV is updated here and restored automatically, thread-safely
end
end Defensive patterns
Strategy: validation
Validate before calling
saved = Dotenv::Diff.new.a Dotenv.restore(saved) if Thread.current == Thread.main
Try / catch
begin
Dotenv.restore(snapshot)
rescue ThreadError
# ENV is process-global; defer the mutation to the main thread
main_thread_queue << -> { Dotenv.restore(snapshot, safe: true) }
end Prevention
- Prefer Dotenv.modify { } over manual snapshot/restore — it is synchronized and scoped
- Treat ENV as read-only after boot in any threaded code (jobs, request threads)
- Before calling restore off the main thread, ask whether the process has other threads reading ENV; if unsure, do not pass safe: true
When it happens
Trigger: Calling `Dotenv.restore` (no `safe:` argument) from a non-main thread: inside Sidekiq/Puma worker threads, Concurrent::Ruby tasks, RSpec threads, or test teardown hooks that snapshot/restore ENV while running under parallelized or threaded test runners.
Common situations: Test suites upgraded to dotenv 3.x that call restore in after-hooks executing off the main thread; background jobs that reset ENV after temporarily setting keys; code that saved state with a Diff on the main thread but restores it from a worker thread.
Related errors
AI-assisted analysis of bkeepers/dotenv@34156bf400 (2026-08-21).
Data as JSON: /api/errors/af54944b097c94dc.
Report an issue: GitHub.