Tempfile #

Every application that processes data occasionally needs temporary files — buffers for data being processed, places to store intermediate results before an operation finishes, or files created during testing that must be cleaned up afterward. Creating temporary files manually (File.open("/tmp/myapp_#{Time.now.to_i}.tmp", "w")) has several problems: names can collide if multiple processes run concurrently, files aren’t automatically deleted if an error occurs mid-process, and choosing the right directory (writable, on the correct filesystem) isn’t trivial across operating systems. Tempfile solves all these problems — it creates files with unique names guaranteed not to collide, in the proper temporary directory for the running OS, and automatically deletes them when the object is garbage-collected or when close! is called.

Creating Tempfiles #

require "tempfile"

# The simplest tempfile
tmp = Tempfile.new
# => #<Tempfile:/tmp/20240115-12345-1a2b3c>
# The file name contains the PID and a random string — guaranteed unique

tmp.path   # => "/tmp/20240115-12345-1a2b3c"
tmp.size   # => 0  (empty file)

# With a prefix — helps identification when debugging
tmp = Tempfile.new("report")
tmp.path   # => "/tmp/report20240115-12345-1a2b3c"

# With a prefix and suffix (two-element array)
tmp = Tempfile.new(["data_import", ".csv"])
tmp.path   # => "/tmp/data_import20240115-12345-1a2b3c.csv"

# In a specific directory
tmp = Tempfile.new("cache", "/var/cache/myapp")
tmp.path   # => "/var/cache/myapp/cache20240115-12345-1a2b3c"

# Tempfile implements IO — all IO methods are available
tmp = Tempfile.new(["upload", ".jpg"])
tmp.write("image data here")
tmp.flush    # make sure the data is written to disk
tmp.rewind   # back to the beginning of the file
tmp.read     # => "image data here"
tmp.size     # => 19

Lifecycle and Automatic Deletion #

This is the most important aspect of Tempfile and often misunderstood — when the file is deleted and how to ensure it definitely gets deleted.

require "tempfile"

# ANTI-PATTERN: creating a Tempfile without a block
tmp = Tempfile.new("data")
tmp.write("important content")
# ... do something with tmp ...
tmp.close   # Closes the file handle, BUT the file still exists on disk!
# If an exception occurs before close, the file isn't deleted!

# Manual alternatives
tmp.close!  # close + delete the file
# or
tmp.unlink  # delete the file (the file handle is still open)
tmp.close

# CORRECT: use a block — the file is guaranteed deleted after the block
Tempfile.create("data") do |tmp|
  tmp.write("important content")
  tmp.flush

  # Do operations with the file
  process_file(tmp.path)
end
# The file is automatically deleted after the block, even on exceptions!

# Tempfile.new with manual ensure (if access outside the block is needed)
tmp = Tempfile.new("data")
begin
  tmp.write("content")
  tmp.flush
  use_file(tmp.path)
ensure
  tmp.close!   # always delete, even on exceptions
end
flowchart TD
    A[Tempfile.create] --> B{Use a block?}
    B -- Yes --> C[File created]
    C --> D[Block executes]
    D --> E{Exception?}
    E -- Yes --> F[File still deleted]
    E -- No --> G[File deleted normally]
    B -- No --> H[Tempfile.new]
    H --> I[File created]
    I --> J[Use the file]
    J --> K{close! called?}
    K -- Yes --> L[File deleted]
    K -- No --> M[File deleted at GC\nor process end]
    M --> N[But the timing isn't guaranteed!]

Tempfile.create vs Tempfile.new #

require "tempfile"

# Tempfile.create — recommended for Ruby 2.6+
# Automatically deleted at the end of the block (or program end without a block)
Tempfile.create(["prefix", ".ext"]) do |file|
  file.write("data")
  file.path   # use this path for other operations
end
# the file is already deleted here

# Tempfile.new — older, needs manual management
# The finalizer automatically deletes at GC, but the timing is unpredictable
tmp = Tempfile.new("prefix")
# ... use it ...
tmp.close!  # explicitly delete

# For new code: always prefer Tempfile.create with a block

Reading and Writing #

Tempfile inherits from File, which inherits from IO — all IO methods are available.

require "tempfile"

Tempfile.create(["report", ".txt"]) do |tmp|
  # Writing
  tmp.write("First line\n")
  tmp.puts("Second line")
  tmp.print("No newline")

  # Flush to disk before another process reads
  tmp.flush

  # Reading from the beginning
  tmp.rewind
  puts tmp.read    # all content

  tmp.rewind
  tmp.each_line { |line| puts line.chomp }

  # Position
  tmp.pos          # current position in bytes
  tmp.seek(0)      # to the beginning
  tmp.seek(0, IO::SEEK_END)  # to the end

  # Binary files
  tmp.binmode
  tmp.write("\x89PNG\r\n\x1a\n")  # PNG header
end

The Atomic Write Pattern #

One of the most important patterns using Tempfile is atomic write — writing to a temp file then renaming to the target, so the target file is never in a half-written state.

require "tempfile"
require "fileutils"

# ANTI-PATTERN: writing directly to the target file
def save_config_unsafe(path, data)
  File.write(path, data.to_json)
  # If the process crashes mid-write, the target file is corrupt!
end

# CORRECT: atomic write with Tempfile
def save_config(path, data)
  dir = File.dirname(path)

  Tempfile.create("config", dir) do |tmp|
    tmp.write(JSON.pretty_generate(data))
    tmp.flush
    tmp.fsync   # make sure the data truly reaches the disk (not just the OS buffer)

    # rename is an atomic operation on Unix filesystems
    # the target file is never in a half-written state
    FileUtils.mv(tmp.path, path)
  end
  # Tempfile.create won't delete a file that's already been renamed
  # because its path has changed
end

# The same pattern for various formats
def export_csv_safe(path, data)
  Tempfile.create(["export", ".csv"], File.dirname(path)) do |tmp|
    CSV.open(tmp.path, "w") do |csv|
      csv << data.first.keys
      data.each { |row| csv << row.values }
    end
    FileUtils.mv(tmp.path, path)
  end
end

Use in Testing #

Tempfile is very useful in unit tests — you need real files to test against but don’t want to litter the project directory.

require "tempfile"
require "minitest/autorun"

class FileProcessorTest < Minitest::Test
  def setup
    # Create a tempfile accessible during one test
    @tempfile = Tempfile.new(["test_input", ".csv"])
    @tempfile.write("nama,umur\nAlice,28\nBob,35\n")
    @tempfile.flush
    @tempfile.rewind
  end

  def teardown
    @tempfile.close!   # delete after the test finishes
  end

  def test_read_csv
    result = FileProcessor.read(@tempfile.path)
    assert_equal 2, result.length
    assert_equal "Alice", result[0]["nama"]
  end

  def test_process_empty
    Tempfile.create("empty") do |f|
      # an empty file
      result = FileProcessor.read(f.path)
      assert_empty result
    end
  end
end

# With RSpec
RSpec.describe FileProcessor do
  around do |example|
    Tempfile.create(["spec", ".json"]) do |f|
      @file = f
      example.run
    end
  end

  it "reads JSON correctly" do
    @file.write('{"key": "value"}')
    @file.flush
    @file.rewind

    result = FileProcessor.read_json(@file.path)
    expect(result["key"]).to eq("value")
  end
end

Tempfile as a Buffer #

Tempfile is useful as a buffer for large data that doesn’t fit in memory.

require "tempfile"
require "net/http"

# Download a large file to a tempfile, process, then save to the destination
def download_and_process(url, destination)
  uri = URI.parse(url)

  Tempfile.create(["download", File.extname(uri.path)]) do |tmp|
    tmp.binmode

    # Streaming download into the tempfile
    Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
      http.request(Net::HTTP::Get.new(uri)) do |response|
        response.read_body { |chunk| tmp.write(chunk) }
      end
    end

    tmp.flush
    tmp.rewind

    # Process the downloaded file
    process_file(tmp, destination)
  end
end

# A buffer for large data transformations
def transform_large_csv(input_path, output_path)
  Tempfile.create(["transform", ".csv"]) do |buffer|
    # Read input, transform, write to the buffer first
    CSV.foreach(input_path, headers: true) do |row|
      transformed = transform(row.to_h)
      buffer.puts(transformed.values.join(","))
    end

    buffer.flush

    # When done, atomic move to the output
    FileUtils.mv(buffer.path, output_path)
  end
end

Integration with Pathname #

Tempfile can be combined with Pathname for more expressive operations.

require "tempfile"
require "pathname"

# Get a Pathname from a Tempfile
Tempfile.create(["data", ".json"]) do |tmp|
  path = Pathname.new(tmp.path)

  tmp.write('{"key": "value"}')
  tmp.flush

  # Now all Pathname methods can be used
  path.size        # => 17
  path.extname     # => ".json"
  path.dirname     # => Pathname("/tmp")
  path.exist?      # => true

  # Copy to another location using Pathname
  output = Pathname.new("/tmp/output.json")
  FileUtils.cp(path.to_s, output.to_s)
end

# Helper to get a Tempfile as a Pathname
def tempfile_path(prefix, suffix = "")
  tmp = Tempfile.new([prefix, suffix])
  Pathname.new(tmp.path).tap do |path|
    ObjectSpace.define_finalizer(path, proc { tmp.close! })
  end
end

Case Study: File Upload Processing #

A common pattern in web applications — uploaded files need processing before being stored.

require "tempfile"

class UploadProcessor
  def self.process(uploaded_file, type:)
    # Save the upload to a tempfile first
    Tempfile.create(["upload", File.extname(uploaded_file.original_filename)]) do |tmp|
      tmp.binmode
      tmp.write(uploaded_file.read)
      tmp.flush
      tmp.rewind

      case type
      when :image
        process_image(tmp.path)
      when :csv
        process_csv(tmp.path)
      when :pdf
        process_pdf(tmp.path)
      end
    end
    # The tempfile is automatically deleted, no files left behind
  end

  private

  def self.process_image(path)
    # Resize, compress, generate thumbnails, etc.
    result_path = "/var/uploads/images/#{SecureRandom.uuid}.jpg"

    # Process the image into result_path
    # ImageMagick, libvips, etc. work with file paths
    `convert #{path} -resize 800x600 #{result_path}`

    result_path
  end

  def self.process_csv(path)
    rows = []
    CSV.foreach(path, headers: true) do |row|
      rows << row.to_h
    end
    rows
  end
end

Summary #

  • Always use Tempfile.create with a block — the file is guaranteed deleted after the block finishes, even if an exception occurs mid-process.
  • Tempfile.new needs manual close! — if not using a block, use ensure to call close! (not just close) so the file is truly deleted.
  • tmp.flush before another process reads the file — written data may still be in the OS buffer; flush ensures the data is on disk before its path is used.
  • Create tempfiles in the same directory as the target — for the atomic write pattern with File.rename, source and destination must be on the same filesystem; create the tempfile in File.dirname(target_path).
  • fsync for critical dataflush only empties Ruby’s buffer into the OS; fsync ensures the data reaches the storage hardware (slower but safer for important data).
  • Ideal for testing — create a tempfile in setup, delete it in teardown; this ensures tests leave no leftover files and don’t depend on pre-existing files.
  • Use as a streaming buffer — for large files that don’t fit in memory, stream the data to a Tempfile before processing; more efficient than building a huge string in memory.
  • Tempfile.create is the modern pattern — safer than Tempfile.new because of more predictable lifecycle; use it for new code.

← Previous: Net::HTTP   Next: Benchmark →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact