diff --git a/components/net/sf/briar/db/DatabaseCleaner.java b/components/net/sf/briar/db/DatabaseCleaner.java
index 32ecfe316d1814bed034232cc3442bae2ba735c7..639c92d4e34a47f979e4cf4b3843ec666df49798 100644
--- a/components/net/sf/briar/db/DatabaseCleaner.java
+++ b/components/net/sf/briar/db/DatabaseCleaner.java
@@ -19,9 +19,9 @@ interface DatabaseCleaner {
 		/**
 		 * Checks how much free storage space is available to the database, and
 		 * if necessary expires old messages until the free space is at least
-		 * DatabaseConstants.MIN_FREE_SPACE. While the free space is less than
-		 * DatabaseConstants.CRITICAL_FREE_SPACE, operations that attempt to
-		 * store messages in the database will block.
+		 * DatabaseConstants.MIN_FREE_SPACE. If the free space is less than
+		 * DatabaseConstants.CRITICAL_FREE_SPACE and there are no more messages
+		 * to expire, an Error will be thrown.
 		 */
 		void checkFreeSpaceAndClean() throws DbException;
 
diff --git a/components/net/sf/briar/db/DatabaseCleanerImpl.java b/components/net/sf/briar/db/DatabaseCleanerImpl.java
index 71574267edf86ac0820520838905a6dd02deb365..71322acf822aaaf623eb6b68aee6aed7164e78ea 100644
--- a/components/net/sf/briar/db/DatabaseCleanerImpl.java
+++ b/components/net/sf/briar/db/DatabaseCleanerImpl.java
@@ -33,12 +33,10 @@ class DatabaseCleanerImpl extends TimerTask implements DatabaseCleaner {
 				callback.checkFreeSpaceAndClean();
 			}
 		} catch(DbException e) {
-			if(LOG.isLoggable(Level.WARNING))
-				LOG.warning(e.toString());
+			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
 			throw new Error(e); // Kill the application
 		} catch(RuntimeException e) {
-			if(LOG.isLoggable(Level.WARNING))
-				LOG.warning(e.toString());
+			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
 			throw new Error(e); // Kill the application
 		}
 	}
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 408d415095733180af9fd969556a564cae429e8e..ebe17a3097950d204503744ee12830df68567ebc 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -1548,8 +1548,7 @@ DatabaseCleaner.Callback {
 			if(freeSpace < CRITICAL_FREE_SPACE && !expired) {
 				// FIXME: Work out what to do here - the amount of free space
 				// is critically low and there are no messages left to expire
-				System.err.println("Disk space is critical - shutting down");
-				System.exit(1);
+				throw new Error("Disk space is critical");
 			}
 			Thread.yield();
 			freeSpace = db.getFreeSpace();
diff --git a/components/net/sf/briar/db/DatabaseConstants.java b/components/net/sf/briar/db/DatabaseConstants.java
index fa013a562d114d3d4657d57bf9150c5d968b1e5a..d177b8c0893197a6ef73d4d0d6cd6827943e46da 100644
--- a/components/net/sf/briar/db/DatabaseConstants.java
+++ b/components/net/sf/briar/db/DatabaseConstants.java
@@ -14,7 +14,7 @@ interface DatabaseConstants {
 	/**
 	 * The minimum amount of space in bytes that must be kept free on the device
 	 * where the database is stored. If less than this much space is free and
-	 * there are no more messages to expire, the program will shut down.
+	 * there are no more messages to expire, an Error will be thrown.
 	 */
 	static final long CRITICAL_FREE_SPACE = 100 * 1024 * 1024; // 100 MiB
 
diff --git a/components/net/sf/briar/db/KeyRotator.java b/components/net/sf/briar/db/KeyRotator.java
new file mode 100644
index 0000000000000000000000000000000000000000..200a1ed24f1f3f9b1f074aa3ebda8954558b0796
--- /dev/null
+++ b/components/net/sf/briar/db/KeyRotator.java
@@ -0,0 +1,24 @@
+package net.sf.briar.db;
+
+import net.sf.briar.api.db.DbException;
+
+interface KeyRotator {
+
+	/**
+	 * Starts a new thread to rotate keys periodically. The rotator will pause
+	 * for the given number of milliseconds between rotations.
+	 */
+	void startRotating(Callback callback, long msBetweenRotations);
+
+	/** Tells the rotator thread to exit. */
+	void stopRotating();
+
+	interface Callback {
+
+		/**
+		 * Rotates keys, replacing and destroying any keys that have passed the
+		 * ends of their respective retention periods.
+		 */
+		void rotateKeys() throws DbException;
+	}
+}
diff --git a/components/net/sf/briar/db/KeyRotatorImpl.java b/components/net/sf/briar/db/KeyRotatorImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..26db0228b3b5392ba856b6b99135a16e550344f9
--- /dev/null
+++ b/components/net/sf/briar/db/KeyRotatorImpl.java
@@ -0,0 +1,41 @@
+package net.sf.briar.db;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.sf.briar.api.db.DbException;
+
+class KeyRotatorImpl extends TimerTask implements KeyRotator {
+
+	private static final Logger LOG =
+			Logger.getLogger(KeyRotatorImpl.class.getName());
+
+	private volatile Callback callback = null;
+	private volatile Timer timer = null;
+
+	public void startRotating(Callback callback, long msBetweenRotations) {
+		this.callback = callback;
+		timer = new Timer();
+		timer.scheduleAtFixedRate(this, 0L, msBetweenRotations);
+	}
+
+	public void stopRotating() {
+		if(timer == null) throw new IllegalStateException();
+		timer.cancel();
+	}
+
+	public void run() {
+		if(callback == null) throw new IllegalStateException();
+		try {
+			callback.rotateKeys();
+		} catch(DbException e) {
+			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
+			throw new Error(e); // Kill the application
+		} catch(RuntimeException e) {
+			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
+			throw new Error(e); // Kill the application
+		}
+	}
+}
diff --git a/test/net/sf/briar/db/KeyRotatorImplTest.java b/test/net/sf/briar/db/KeyRotatorImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2470e534e6c1fb7da634552a375888451a96e12e
--- /dev/null
+++ b/test/net/sf/briar/db/KeyRotatorImplTest.java
@@ -0,0 +1,53 @@
+package net.sf.briar.db;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.db.KeyRotator.Callback;
+
+import org.junit.Test;
+
+public class KeyRotatorImplTest extends BriarTestCase {
+
+	@Test
+	public void testCleanerRunsPeriodically() throws Exception {
+		final CountDownLatch latch = new CountDownLatch(5);
+		Callback callback = new Callback() {
+
+			public void rotateKeys() throws DbException {
+				latch.countDown();
+			}
+		};
+		KeyRotatorImpl cleaner = new KeyRotatorImpl();
+		// Start the rotator
+		cleaner.startRotating(callback, 10L);
+		// The keys should be rotated five times (allow 5 secs for system load)
+		assertTrue(latch.await(5, TimeUnit.SECONDS));
+		// Stop the rotator
+		cleaner.stopRotating();
+	}
+
+	@Test
+	public void testStoppingCleanerWakesItUp() throws Exception {
+		final CountDownLatch latch = new CountDownLatch(1);
+		Callback callback = new Callback() {
+
+			public void rotateKeys() throws DbException {
+				latch.countDown();
+			}
+		};
+		KeyRotatorImpl cleaner = new KeyRotatorImpl();
+		long start = System.currentTimeMillis();
+		// Start the rotator
+		cleaner.startRotating(callback, 10L * 1000L);
+		// The keys should be rotated once at startup
+		assertTrue(latch.await(5, TimeUnit.SECONDS));
+		// Stop the rotator (it should be waiting between rotations)
+		cleaner.stopRotating();
+		long end = System.currentTimeMillis();
+		// Check that much less than 10 seconds expired
+		assertTrue(end - start < 10L * 1000L);
+	}
+}