diff --git a/NEWS b/NEWS
index 28d22722d..d70b04690 100644
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,7 @@ ver 0.20 (not yet released)
 * mixer
   - null: new plugin
 * reset song priority on playback
+* write database and state file atomically
 * remove dependency on GLib
 
 ver 0.19.8 (not yet released)
diff --git a/configure.ac b/configure.ac
index 79a4952b0..94ce62e73 100644
--- a/configure.ac
+++ b/configure.ac
@@ -219,7 +219,7 @@ AC_SEARCH_LIBS([socket], [socket])
 AC_SEARCH_LIBS([gethostbyname], [nsl])
 
 if test x$host_is_linux = xyes; then
-	AC_CHECK_FUNCS(pipe2 accept4)
+	AC_CHECK_FUNCS(pipe2 accept4 linkat)
 fi
 
 AC_CHECK_FUNCS(getpwnam_r getpwuid_r)
diff --git a/src/fs/io/FileOutputStream.cxx b/src/fs/io/FileOutputStream.cxx
index 2951149b9..7a6416557 100644
--- a/src/fs/io/FileOutputStream.cxx
+++ b/src/fs/io/FileOutputStream.cxx
@@ -79,14 +79,47 @@ FileOutputStream::Cancel()
 #include <unistd.h>
 #include <errno.h>
 
-FileOutputStream::FileOutputStream(Path _path, Error &error)
-	:path(_path),
-	 fd(OpenFile(path,
-		     O_WRONLY|O_CREAT|O_TRUNC,
-		     0666))
+#ifdef HAVE_LINKAT
+#ifndef O_TMPFILE
+/* supported since Linux 3.11 */
+#define __O_TMPFILE 020000000
+#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
+#include <stdio.h>
+#endif
+
+/**
+ * Open a file using Linux's O_TMPFILE for writing the given file.
+ */
+static int
+OpenTempFile(Path path)
 {
-	if (fd < 0)
-		error.FormatErrno("Failed to create %s", path.c_str());
+	const auto directory = path.GetDirectoryName();
+	if (directory.IsNull())
+		return -1;
+
+	return OpenFile(directory, O_TMPFILE|O_WRONLY, 0666);
+}
+
+#endif /* HAVE_LINKAT */
+
+FileOutputStream::FileOutputStream(Path _path, Error &error)
+	:path(_path)
+{
+#ifdef HAVE_LINKAT
+	/* try Linux's O_TMPFILE first */
+	fd = OpenTempFile(path);
+	is_tmpfile = fd >= 0;
+	if (!is_tmpfile) {
+#endif
+		/* fall back to plain POSIX */
+		fd = OpenFile(path,
+			      O_WRONLY|O_CREAT|O_TRUNC,
+			      0666);
+		if (fd < 0)
+			error.FormatErrno("Failed to create %s", path.c_str());
+#ifdef HAVE_LINKAT
+	}
+#endif
 }
 
 bool
@@ -112,6 +145,22 @@ FileOutputStream::Commit(Error &error)
 {
 	assert(IsDefined());
 
+#if HAVE_LINKAT
+	if (is_tmpfile) {
+		RemoveFile(path);
+
+		/* hard-link the temporary file to the final path */
+		char fd_path[64];
+		snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd);
+		if (linkat(AT_FDCWD, fd_path, AT_FDCWD, path.c_str(),
+			   AT_SYMLINK_FOLLOW) < 0) {
+			error.FormatErrno("Failed to commit %s", path.c_str());
+			close(fd);
+			return false;
+		}
+	}
+#endif
+
 	bool success = close(fd) == 0;
 	fd = -1;
 	if (!success)
@@ -128,7 +177,10 @@ FileOutputStream::Cancel()
 	close(fd);
 	fd = -1;
 
-	RemoveFile(path);
+#ifdef HAVE_LINKAT
+	if (!is_tmpfile)
+#endif
+		RemoveFile(path);
 }
 
 #endif
diff --git a/src/fs/io/FileOutputStream.hxx b/src/fs/io/FileOutputStream.hxx
index 5ac2f9e15..03d062134 100644
--- a/src/fs/io/FileOutputStream.hxx
+++ b/src/fs/io/FileOutputStream.hxx
@@ -42,6 +42,14 @@ class FileOutputStream final : public OutputStream {
 	int fd;
 #endif
 
+#ifdef HAVE_LINKAT
+	/**
+	 * Was O_TMPFILE used?  If yes, then linkat() must be used to
+	 * create a link to this file.
+	 */
+	bool is_tmpfile;
+#endif
+
 public:
 	FileOutputStream(Path _path, Error &error);