{"record":{"id":"af54944b097c94dc","repo":"bkeepers/dotenv","slug":"dotenv-restore-is-not-thread-safe-use-dotenv-mod","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"ThreadError","httpStatus":null,"severity":"error","filePath":"lib/dotenv.rb","lineNumber":85,"sourceCode":"  def save\n    instrument(:save) do |payload|\n      @diff = payload[:diff] = Dotenv::Diff.new\n    end\n  end\n\n  # Restore `ENV` to a given state\n  #\n  # @param env [Hash] Hash of keys and values to restore, defaults to the last saved state\n  # @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error.\n  def restore(env = @diff&.a, safe: Thread.current == Thread.main)\n    # No previously saved or provided state to restore\n    return unless env\n\n    diff = Dotenv::Diff.new(b: env)\n    return unless diff.any?\n\n    unless safe\n      raise ThreadError, <<~EOE.tr(\"\\n\", \" \")\n        Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration\n        of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore\n        this error.\n      EOE\n    end\n    instrument(:restore, diff: diff) { ENV.replace(env) }\n  end\n\n  # Update `ENV` with the given hash of keys and values\n  #\n  # @param env [Hash] Hash of keys and values to set in `ENV`\n  # @param overwrite [Boolean|:warn] Overwrite existing `ENV` values\n  def update(env = {}, overwrite: false)\n    instrument(:update) do |payload|\n      diff = payload[:diff] = Dotenv::Diff.new do\n        ENV.update(env.transform_keys(&:to_s)) do |key, old_value, new_value|\n          # This block is called when a key exists. Return the new value if overwrite is true.\n          case overwrite","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/bkeepers/dotenv/blob/34156bf400cd67387fa6ed9f146778f6a2f5f743/lib/dotenv.rb#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"# before\nsnapshot = Dotenv::Diff.new.a\nThread.new do\n  Dotenv.restore(snapshot) # raises ThreadError\nend\n\n# after\nThread.new do\n  Dotenv.modify(\"API_KEY\" => \"temp\") do\n    # ENV is updated here and restored automatically, thread-safely\n  end\nend","handlingStrategy":"validation","validationCode":"saved = Dotenv::Diff.new.a\nDotenv.restore(saved) if Thread.current == Thread.main","typeGuard":null,"tryCatchPattern":"begin\n  Dotenv.restore(snapshot)\nrescue ThreadError\n  # ENV is process-global; defer the mutation to the main thread\n  main_thread_queue << -> { Dotenv.restore(snapshot, safe: true) }\nend","preventionTips":["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"],"tags":["thread-safety","ruby","dotenv","environment-variables","concurrency"],"backgroundTag":"thread-safety-violation","analyzedSha":"34156bf400cd67387fa6ed9f146778f6a2f5f743","analyzedAt":"2026-08-21T19:00:36.354Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}