skip to content

In Ruby, why can a long-running service using Tempfile.new fill the temp directory, and how do Tempfile.create and Dir.mktmpdir make cleanup predictable?

level: seniorimportance: should knowfreq 30%

answer

  1. Tempfile.new: removed by a GC finalizer
  2. SIGKILL leaves files behind
  3. Tempfile.create with a block
  4. anonymous: true since 3.4
  5. Dir.mktmpdir removes the tree after the block

basics

~20 s

Unless you unlink it, Tempfile.new deletes its file only when the Tempfile is garbage collected or the process exits normally, so files pile up. Tempfile.create with a block and Dir.mktmpdir with a block delete as soon as the block ends; anonymous: true removes the name immediately.

solid answer

~40 s

`Tempfile.new` returns a `Tempfile` whose file is removed by a **finalizer** — when the garbage collector reclaims the object or the interpreter exits. Long GC intervals let files accumulate, and a process killed by `SIGKILL` or a crash leaves them forever. The tempfile rdoc itself calls `Tempfile.create` the recommended form: with a block it yields a plain `File` (mode `w+`, permissions `0600`) and removes the file when the block exits, returning the block's value; without a block you must close and `File.unlink` it. Since Ruby 3.4, `Tempfile.create(anonymous: true)` removes the directory entry right away (via `O_TMPFILE` on Linux), so the space is reclaimed even after an abnormal exit. `Dir.mktmpdir { |dir| ... }` creates a `0700` directory and deletes it and its contents with `FileUtils.remove_entry` after the block.

code

ruby · 24 lines
ruby
require "tempfile"
require "tmpdir"

image_bytes = File.binread("upload.png")

# Named file for an external tool; removed when the block exits
size = Tempfile.create(["upload", ".png"]) do |f|
  f.binmode
  f.write(image_bytes)
  f.flush
  system("optipng", f.path)
  File.size(f.path)
end

# Scratch buffer with no name at all (Ruby 3.4+)
Tempfile.create(anonymous: true) do |buf|
  buf.write("x" * 1_000_000)
  buf.rewind
end

# A whole working directory, removed with its contents
Dir.mktmpdir("resize") do |dir|
  File.write(File.join(dir, "a.txt"), "data")
end

go deeper

for a junior

Recall require "tempfile" and require "tmpdir", and that the block forms of Tempfile.create and Dir.mktmpdir clean up for you.

for a middle

Explain that Tempfile.new relies on a GC finalizer, what close and close! do, and what Tempfile.create without a block leaves behind.

for a senior

Diagnose a filling temp directory in a long-running service, switch to block forms or anonymous: true, and account for killed processes that never run finalizers.

for a principal

Decide how services bound scratch-disk usage, such as dedicated temp mounts, periodic sweeps and anonymous files, rather than trusting every call site to clean up.

## Two ways to make a temporary file `require "tempfile"` gives you two creation styles, and the library's own documentation ranks them: - **`Tempfile.create`** — *recommended*. Returns an ordinary `File`. Deletion timing is predictable. - **`Tempfile.new` / `Tempfile.open`** — kept mostly for backward compatibility. Returns a `Tempfile` (a delegator around `File`) whose file is removed by the garbage collector's finalizer. Both create a uniquely named file in the system temporary directory (`Dir.tmpdir`), open for reading and writing with permissions `0600`, and pick names safely across threads and processes. ## Why Tempfile.new leaks in production A `Tempfile` object registers a finalizer that deletes its file. That means: 1. The file is deleted when the GC collects the object or when Ruby exits normally — not when you are done with it. 2. In a busy service with long GC intervals, thousands of finished temp files can sit on disk at once. 3. If the process dies from `SIGKILL`, an out-of-memory kill or a segfault, finalizers never run and the files stay forever. 4. The legacy fix is manual: `ensure file.close; file.unlink` (or `file.close!`, which does both). A worker that converts uploaded images with `Tempfile.new` and relies on GC is exactly how a temp directory fills up over weeks. ## Tempfile.create: deletion you can predict | Form | When the file is removed | |---|---| | `Tempfile.create { \|f\| ... }` | when the block exits, even on an exception | | `Tempfile.create` (no block) | never automatically — close it and `File.unlink(f.path)` | | `Tempfile.create(anonymous: true)` | before it returns; no name ever visible | | `Tempfile.create(anonymous: true) { ... }` | before the block runs | With a block, `Tempfile.create` returns the **block's value**, closes the file and removes it. The object is a plain `File`, so there is no delegation overhead. You can pass a `[prefix, suffix]` basename such as `["upload", ".jpg"]` when a tool needs the right extension. `anonymous: true` (added in Ruby 3.4) creates the file and removes its directory entry at once — `O_TMPFILE` on Linux, `FILE_SHARE_DELETE` on Windows. The open handle still works; `f.path` returns only the temp directory. Because nothing has a name, the operating system reclaims the space when the handle closes or the process ends, **even after an abnormal exit**. Use it for large scratch buffers that no other process needs to open. ## Dir.mktmpdir for several files `require "tmpdir"` adds `Dir.mktmpdir`: - `Dir.mktmpdir { |dir| ... }` creates a new directory with permissions `0700`, yields its path, and after the block removes the directory **and everything in it** with `FileUtils.remove_entry`, returning the block's value. - `Dir.mktmpdir("resize")` sets a name prefix; a second argument chooses the parent directory. - Without a block it returns the path and removes nothing; the caller must clean up in an `ensure`. It is the right tool when an external command writes several outputs, or when a test needs an isolated working tree. ## Temporary files in tests Test suites are where temp files multiply fastest, because every example may create some: 1. Create a per-example directory with `Dir.mktmpdir` in the setup hook and remove it in teardown, or wrap the example body in the block form so removal is automatic. 2. Point the code under test at that directory through a parameter or configuration value instead of letting it write into the project tree. 3. Prefer `Tempfile.create` with a block for single files, so a failing assertion inside the block still removes the file. `Dir.mktmpdir` directories are created with mode `0700`, so other users on the machine cannot read them, and each call gets a unique name, so parallel test processes never share a directory by accident. ## Choosing - One scratch file you read back yourself: `Tempfile.create(anonymous: true) { ... }`. - A named file another program must open: `Tempfile.create(["in", ".png"]) { |f| ... }`. - Several files or a working directory: `Dir.mktmpdir { |dir| ... }`. - `Tempfile.new` only when an API insists on a `Tempfile`, and then with `close!` in an `ensure`.

  • If you must use Tempfile.new, how do you make sure the file is deleted?
    Wrap the work in `begin ... ensure` and call `file.close!`, which closes and unlinks, or `file.close` followed by `file.unlink`. On POSIX systems you can also unlink right after creating it and keep using the open handle, so the name disappears at once and the space is freed when the handle closes.
  • What does Dir.mktmpdir return, and what happens to the directory when you call it without a block?
    With a block it returns the block's value and removes the directory and all its contents afterwards. Without a block it returns the directory's path and removes nothing, so the caller must delete it, typically with `FileUtils.remove_entry(dir)` in an `ensure`.

saying these in an interview costs you the question

  • Tempfile.new deletes its file as soon as you call close.
  • Temp files are always cleaned up, even if the process is killed.
  • Tempfile.create returns a Tempfile that is removed by the GC.
  • Dir.mktmpdir with a block leaves the directory for the caller to delete.
  • Tempfile.create(anonymous: true) returns a file whose path other processes can open.