Skip to content
CalliCoder

How to Create a Temp File or Directory in Java

Java 13 min read

Files.createTempFile creates with owner-only permissions and File.createTempFile does not, deleteOnExit leaks until the JVM stops, and the shared temp directory is a hostile place to write.

There are two createTempFile methods in the JDK. They look equivalent, and on a shared machine one of them creates a world-readable file. That difference, plus deleteOnExit retaining every filename until the JVM stops, is most of what there is to say about temporary files.

Written against Java 17.

The NIO version

Path temp = Files.createTempFile("upload-", ".tmp");
// /tmp/upload-13887044719544927856.tmp

The prefix and suffix are hints; the middle is a random number. Either may be null, in which case .tmp is used as the suffix and nothing as the prefix.

On a POSIX filesystem this creates the file with rw-------, owner only, as part of the creation, not as a follow-up call. That is the whole security argument for it, and it is not documented as prominently as it deserves.

To create in a specific directory rather than the system temp directory:

Path temp = Files.createTempFile(Path.of("/var/data"), "export-", ".csv");

That form matters for more than tidiness: a temporary file used as the staging half of an atomic write has to be on the same filesystem as its target, and the system temp directory usually is not.

The legacy version, and why not to use it

File temp = File.createTempFile("upload-", ".tmp");

Same signature, different permissions. java.io.File.createTempFile creates the file with the default permissions the umask allows, typically rw-r--r--, readable by every user on the machine.

On a single-user container that is harmless. On a shared host, or in any process handling data belonging to someone else. It means an uploaded document sits in /tmp readable by anyone with a shell. Nothing warns you, and the file is deleted soon enough that the exposure is easy to miss.

Use Files.createTempFile. temp.toFile() converts if a legacy API needs a File.

Temp directories

Path dir = Files.createTempDirectory("job-");
// /tmp/job-4823905810214

Path dirHere = Files.createTempDirectory(Path.of("/var/work"), "job-");

Created with rwx------ on POSIX, which is what makes it safe to write several files into: the directory is private, so the individual files inside it are unreachable regardless of their own permissions.

That is the pattern for anything producing more than one temporary file (one private directory, then ordinary files inside it) rather than several individually secured files in the shared temp directory.

Cleaning up

deleteOnExit is the obvious answer and the wrong one:

temp.toFile().deleteOnExit();

Two problems. The filename is added to a list held by the JVM for its entire lifetime, so a long-running server that creates a temp file per request accumulates one string per request and never releases any of them: a slow leak that looks like an unrelated memory problem. And the hook only runs on a normal shutdown: kill -9, a container stop that times out, or Runtime.halt all skip it, leaving the files behind anyway.

deleteOnExit is defensible for a short-lived command-line tool. In a server it is a leak with an unreliable payoff.

Delete explicitly instead, in a finally or with try-with-resources:

Path temp = Files.createTempFile("upload-", ".tmp");
try {
    process(temp);
} finally {
    Files.deleteIfExists(temp);
}

For a directory, the delete is a reverse-ordered walk, because a directory must be empty first:

static void deleteRecursively(Path dir) throws IOException {
    try (Stream<Path> walk = Files.walk(dir)) {
        walk.sorted(Comparator.reverseOrder()).forEach(p -> {
            try { Files.delete(p); } catch (IOException e) { throw new UncheckedIOException(e); }
        });
    }
}

DELETE_ON_CLOSE covers the case where the file is only needed while it is open:

try (var channel = FileChannel.open(temp, StandardOpenOption.WRITE,
                                          StandardOpenOption.DELETE_ON_CLOSE)) {
    // the file disappears when the channel closes, including on an exception
}

That is the strongest of the three, because closing happens on the abnormal path too.

Where the files go

System.getProperty("java.io.tmpdir");   // /tmp, /var/folders/... , C:\Users\...\AppData\Local\Temp

It can be overridden at startup with -Djava.io.tmpdir=/var/app/tmp, which is worth doing in a container: the default is often a small tmpfs living in RAM, and a large upload spooled there consumes memory rather than disk and can be killed by the out-of-memory killer.

Reading the property is fine; assuming /tmp is not. It is /var/folders/... on macOS and a per-user path under AppData on Windows.

What makes the shared temp directory hostile

On a multi-user system, /tmp is world-writable with the sticky bit set: anyone can create entries, only the owner can delete their own. That enables one specific attack: an attacker creates a symbolic link at a name your program is about to use, and your write follows it somewhere else.

Files.createTempFile defends against this by generating an unpredictable name and creating the file atomically, failing if the name already exists. What does not defend against it is constructing the name yourself:

Path bad = Path.of(System.getProperty("java.io.tmpdir"), "export-" + userId + ".csv");

That name is predictable, so the file can be pre-created as a link. It is also a collision between two concurrent requests for the same user. Never build a temp filename from application data: let the API generate it and keep the returned Path.

Spooling large input

The usual reason to want a temporary file is that something too large to hold in memory has arrived and has to be processed in more than one pass. That is a real need, and it has two failure modes worth designing against.

The first is that a temp file has no size limit of its own. An upload spooled to disk without a cap fills the volume, and on a container with an ephemeral overlay filesystem that takes down the whole process rather than one request. Count the bytes as you copy and stop:

try (InputStream in = request.getInputStream();
     OutputStream out = Files.newOutputStream(temp)) {
    long copied = in.transferTo(out);
    if (copied > MAX_BYTES) {
        throw new IllegalStateException("Payload too large");
    }
}

Checking after the copy is the wrong order, the disk is already full. A counting wrapper around the stream that throws as soon as the limit is passed is the version that actually protects anything.

The second is concurrency. Several requests spooling at once multiply the disk use, so the limit that matters is the per-request cap times the concurrency, not the cap alone. A semaphore bounding how many spools may run at once is often simpler than trying to size the volume for the worst case.

Testing with temporary files

JUnit 5 supplies a temp directory per test and deletes it afterwards:

@Test
void writesTheExport(@TempDir Path dir) throws IOException {
    Path out = dir.resolve("export.csv");
    exporter.write(out);
    assertThat(Files.readString(out)).startsWith("id,name");
}

@TempDir on a parameter gives one per test method; on a static field, one per class. The cleanup attribute controls the deletion policy, and CleanupMode.ON_SUCCESS is worth knowing about: it keeps the directory when the test fails, so the files are still there to look at. This is strictly better than creating temp files in the test body, because cleanup happens even when the test fails, which is exactly when a leftover file confuses the next run.

Related: creating a file and checking whether one exists. More in the Java guides.

Frequently asked questions

What is the difference between Files.createTempFile and File.createTempFile?

Permissions. The NIO version creates the file rw------- on POSIX systems; the legacy version uses the default umask, typically leaving it world-readable.

Where are temporary files created?

In java.io.tmpdir unless a directory is passed. Override it with -Djava.io.tmpdir=..., which is worth doing in containers where the default is a RAM-backed filesystem.

Should I use deleteOnExit?

Not in a long-running process. It retains every filename for the JVM’s lifetime and only runs on a clean shutdown. Delete in a finally block instead.

What if the JVM is killed before cleanup?

The file stays. Nothing in the JDK survives kill -9, so anything that must be cleaned needs an external sweep, a startup routine that clears the application’s own temp directory is the usual answer.

How do I create a temporary directory?

Files.createTempDirectory(prefix). It is created rwx------, which makes everything written inside it private without securing each file separately.

How do I delete a temporary directory?

Walk it in reverse order and delete each entry. A directory must be empty before it can be removed. There is no recursive delete in the JDK.

Can I choose the exact filename?

You can create an ordinary file with any name you like, but not through createTempFile, and a predictable name in a shared directory is the vulnerability the random name exists to prevent.

What does DELETE_ON_CLOSE do?

Deletes the file when the channel closes, including when an exception unwinds through a try-with-resources. It is the most reliable of the cleanup options when the file is only needed while open.

Is the temp directory shared between users?

On a typical Linux system, yes, /tmp is world-writable with the sticky bit. That is why the generated name is unpredictable and the creation atomic.

How do I use temporary files in a JUnit test?

@TempDir Path dir as a parameter or a field. JUnit creates it and deletes it afterwards, including when the test fails.