diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
index 5f4f27398849c27a29f78020db7f743557c39fdc..08b5859d9376c71c6c38ed06856019ec3f36fb02 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
@@ -648,15 +648,11 @@ interface Database<T> {
 	void setTransportKeysActive(T txn, TransportId t, KeySetId k)
 			throws DbException;
 
-	void updateLastSentTime(Connection txn, ContactId c, MessageId m)
-			throws DbException;
-
 	/**
-	 * Updates the transmission count and expiry time of the given message
-	 * with respect to the given contact, using the latency of the transport
-	 * over which it was sent.
+	 * Updates the transmission count and the lastSentTme of the given message
+	 * with respect to the given contact.
 	 */
-	void updateExpiryTime(T txn, ContactId c, MessageId m, int maxLatency)
+	void updateLastSentTime(T txn, ContactId c, MessageId m)
 			throws DbException;
 
 	/**
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
index bae66c12caf616e656451b78736723f20a02f31d..45e7ed3e86fd521599746fae3e8937993474a17b 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
@@ -313,11 +313,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		T txn = unbox(transaction);
 		if (!db.containsContact(txn, c))
 			throw new NoSuchContactException();
-		Collection<MessageId> ids = db.getMessagesToSend(txn, c, maxLength, maxLatency);
+		Collection<MessageId> ids =
+				db.getMessagesToSend(txn, c, maxLength, maxLatency);
 		List<byte[]> messages = new ArrayList<>(ids.size());
 		for (MessageId m : ids) {
 			messages.add(db.getRawMessage(txn, m));
-			db.updateExpiryTime(txn, c, m, maxLatency);
+			db.updateLastSentTime(txn, c, m);
 		}
 		if (ids.isEmpty()) return null;
 		db.lowerRequestedFlag(txn, c, ids);
@@ -333,9 +334,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		T txn = unbox(transaction);
 		if (!db.containsContact(txn, c))
 			throw new NoSuchContactException();
-		Collection<MessageId> ids = db.getMessagesToOffer(txn, c, maxMessages, maxLatency);
+		Collection<MessageId> ids =
+				db.getMessagesToOffer(txn, c, maxMessages, maxLatency);
 		if (ids.isEmpty()) return null;
-		for (MessageId m : ids) db.updateExpiryTime(txn, c, m, maxLatency);
+		for (MessageId m : ids) db.updateLastSentTime(txn, c, m);
 		return new Offer(ids);
 	}
 
@@ -367,7 +369,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		List<byte[]> messages = new ArrayList<>(ids.size());
 		for (MessageId m : ids) {
 			messages.add(db.getRawMessage(txn, m));
-			db.updateExpiryTime(txn, c, m, maxLatency);
+			db.updateLastSentTime(txn, c, m);
 		}
 		if (ids.isEmpty()) return null;
 		db.lowerRequestedFlag(txn, c, ids);
@@ -593,7 +595,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	}
 
 	@Override
-	public long getNextSendTime(Transaction transaction, ContactId c, int maxLatency)
+	public long getNextSendTime(Transaction transaction, ContactId c,
+			int maxLatency)
 			throws DbException {
 		T txn = unbox(transaction);
 		return db.getNextSendTime(txn, c, maxLatency);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java b/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java
index 0e2f89bcfa857148d1baa1be9cdedfcf5664e832..b1a9506e93933e7f00a47eeddae0293d68549f7c 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/ExponentialBackoff.java
@@ -15,6 +15,7 @@ class ExponentialBackoff {
 		if (now < 0) throw new IllegalArgumentException();
 		if (maxLatency <= 0) throw new IllegalArgumentException();
 		if (txCount < 0) throw new IllegalArgumentException();
+		if(now == 0) return 0;
 		// The maximum round-trip time is twice the maximum latency
 		long roundTrip = maxLatency * 2L;
 		// The interval between transmissions is roundTrip * 2 ^ txCount
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index 53a34fe4b30f0069b29120e3cd5dcaae0562ad54..afb8f1c32686a75a3348d66e3be8dd1ecdab5842 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -36,6 +36,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -76,7 +77,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
 abstract class JdbcDatabase implements Database<Connection> {
 
 	// Package access for testing
-	static final int CODE_SCHEMA_VERSION = 39;
+	static final int CODE_SCHEMA_VERSION = 40;
 
 	// Rotation period offsets for incoming transport keys
 	private static final int OFFSET_PREV = -1;
@@ -214,7 +215,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " ack BOOLEAN NOT NULL,"
 					+ " seen BOOLEAN NOT NULL,"
 					+ " requested BOOLEAN NOT NULL,"
-					+ " expiry BIGINT NOT NULL,"
+					+ " lastSentTime BIGINT NOT NULL,"
 					+ " txCount INT NOT NULL,"
 					+ " PRIMARY KEY (messageId, contactId),"
 					+ " FOREIGN KEY (messageId)"
@@ -391,7 +392,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	// Package access for testing
 	List<Migration<Connection>> getMigrations() {
-		return singletonList(new Migration38_39());
+		return Arrays.asList(new Migration38_39(), new Migration39_40());
 	}
 
 	private void storeSchemaVersion(Connection txn, int version)
@@ -799,7 +800,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			String sql = "INSERT INTO statuses (messageId, contactId, groupId,"
 					+ " timestamp, length, state, groupShared, messageShared,"
-					+ " deleted, ack, seen, requested, expiry, txCount)"
+					+ " deleted, ack, seen, requested, lastSentTime, txCount)"
 					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, 0, 0)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
@@ -1844,7 +1845,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " AND groupShared = TRUE AND messageShared = TRUE"
 					+ " AND deleted = FALSE"
 					+ " AND seen = FALSE AND requested = FALSE"
-					+ " AND (lastSentTime + ? * POWER(2,txCount+1)) > ?"
+					+ " AND ( lastSentTime = 0 OR (lastSentTime + ? * POWER(2,txCount+1)) < ?)"
 					+ " ORDER BY timestamp LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
@@ -1902,7 +1903,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " AND groupShared = TRUE AND messageShared = TRUE"
 					+ " AND deleted = FALSE"
 					+ " AND seen = FALSE"
-					+ " AND (lastSentTime + ? * POWER(2,txCount+1)) > ?"
+					+ " AND ( lastSentTime = 0 OR (lastSentTime + ? * POWER(2,txCount+1)) < ?)"
 					+ " ORDER BY timestamp";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
@@ -1996,25 +1997,25 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT lastSendTime, txCount FROM statuses"
+			String sql = "SELECT lastSentTime, txCount FROM statuses"
 					+ " WHERE contactId = ? AND state = ?"
 					+ " AND groupShared = TRUE AND messageShared = TRUE"
 					+ " AND deleted = FALSE AND seen = FALSE"
-					+ " ORDER BY lastSendTime LIMIT 1";
+					+ " ORDER BY lastSentTime LIMIT 1";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setInt(2, DELIVERED.getValue());
 			rs = ps.executeQuery();
-			long lastSendTime = Long.MAX_VALUE;
+			long lastSentTime = Long.MAX_VALUE;
 			int txCount = Integer.MAX_VALUE;
 			if (rs.next()) {
-				lastSendTime = rs.getLong(1);
+				lastSentTime = rs.getLong(1);
 				txCount = rs.getInt(2);
 				if (rs.next()) throw new AssertionError();
 			}
 			rs.close();
 			ps.close();
-			return calculateExpiry(lastSendTime, maxLatency, txCount);
+			return calculateExpiry(lastSentTime, maxLatency, txCount);
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -2058,7 +2059,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " AND groupShared = TRUE AND messageShared = TRUE"
 					+ " AND deleted = FALSE"
 					+ " AND seen = FALSE AND requested = TRUE"
-					+ " AND expiry < ?"
+					+ " AND lastSendTime < ?"
 					+ " ORDER BY timestamp";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
@@ -2672,7 +2673,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "UPDATE statuses SET expiry = 0, txCount = 0"
+			String sql = "UPDATE statuses SET lastSentTime = 0, txCount = 0"
 					+ " WHERE messageId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
@@ -2894,41 +2895,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-
-	@Override
-	public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
-			int maxLatency) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT txCount FROM statuses"
-					+ " WHERE messageId = ? AND contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, m.getBytes());
-			ps.setInt(2, c.getInt());
-			rs = ps.executeQuery();
-			if (!rs.next()) throw new DbStateException();
-			int txCount = rs.getInt(1);
-			if (rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			sql = "UPDATE statuses SET expiry = ?, txCount = txCount + 1"
-					+ " WHERE messageId = ? AND contactId = ?";
-			ps = txn.prepareStatement(sql);
-			long now = clock.currentTimeMillis();
-			ps.setLong(1, calculateExpiry(now, maxLatency, txCount));
-			ps.setBytes(2, m.getBytes());
-			ps.setInt(3, c.getInt());
-			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	@Override
 	public void updateTransportKeys(Connection txn, KeySet ks)
 			throws DbException {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Migration39_40.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration39_40.java
new file mode 100644
index 0000000000000000000000000000000000000000..9cf32a54e7503267de6479de34aec3cabb3c7607
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration39_40.java
@@ -0,0 +1,51 @@
+package org.briarproject.bramble.db;
+
+import org.briarproject.bramble.api.db.DbException;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+class Migration39_40 implements Migration<Connection> {
+
+	private static final Logger LOG =
+			Logger.getLogger(Migration39_40.class.getName());
+
+	@Override
+	public int getStartVersion() {
+		return 39;
+	}
+
+	@Override
+	public int getEndVersion() {
+		return 40;
+	}
+
+	@Override
+	public void migrate(Connection txn) throws DbException {
+		Statement s = null;
+		try {
+			s = txn.createStatement();
+			// Add not null constraints
+			s.execute("ALTER TABLE state"
+					+ " ALTER COLUMN expiry RENAME TO lastSentTime");
+		} catch (SQLException e) {
+			tryToClose(s);
+			throw new DbException(e);
+		}
+	}
+
+	private void tryToClose(@Nullable Statement s) {
+		try {
+			if (s != null) s.close();
+		} catch (SQLException e) {
+			logException(LOG, WARNING, e);
+		}
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java
index f4109a48899f06af331f454931d1d21b1ef162ec..c65e563761c752193ab2672399a5394608768598 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java
@@ -279,7 +279,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 				try {
 					b = db.generateRequestedBatch(txn, contactId,
 							MAX_RECORD_PAYLOAD_BYTES, maxLatency);
-					setNextSendTime(db.getNextSendTime(txn, contactId));
+					setNextSendTime(db.getNextSendTime(txn, contactId, maxLatency));
 					db.commitTransaction(txn);
 				} finally {
 					db.endTransaction(txn);
@@ -326,7 +326,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 				try {
 					o = db.generateOffer(txn, contactId, MAX_MESSAGE_IDS,
 							maxLatency);
-					setNextSendTime(db.getNextSendTime(txn, contactId));
+					setNextSendTime(db.getNextSendTime(txn, contactId, maxLatency));
 					db.commitTransaction(txn);
 				} finally {
 					db.endTransaction(txn);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
index 676cac2e3708b0465afe2bc08ccaf3ff7a308988..ecaa30a7b6b4c0727d6bf67973ea42e4a74aeb52 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
@@ -873,16 +873,14 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getMessagesToSend(txn, contactId, size * 2);
+			oneOf(database).getMessagesToSend(txn, contactId, size * 2, maxLatency);
 			will(returnValue(ids));
 			oneOf(database).getRawMessage(txn, messageId);
 			will(returnValue(raw));
-			oneOf(database).updateExpiryTime(txn, contactId, messageId,
-					maxLatency);
+			oneOf(database).updateLastSentTime(txn, contactId, messageId);
 			oneOf(database).getRawMessage(txn, messageId1);
 			will(returnValue(raw1));
-			oneOf(database).updateExpiryTime(txn, contactId, messageId1,
-					maxLatency);
+			oneOf(database).updateLastSentTime(txn, contactId, messageId1);
 			oneOf(database).lowerRequestedFlag(txn, contactId, ids);
 			oneOf(database).commitTransaction(txn);
 			oneOf(eventBus).broadcast(with(any(MessagesSentEvent.class)));
@@ -909,12 +907,10 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getMessagesToOffer(txn, contactId, 123);
+			oneOf(database).getMessagesToOffer(txn, contactId, 123, maxLatency);
 			will(returnValue(ids));
-			oneOf(database).updateExpiryTime(txn, contactId, messageId,
-					maxLatency);
-			oneOf(database).updateExpiryTime(txn, contactId, messageId1,
-					maxLatency);
+			oneOf(database).updateLastSentTime(txn, contactId, messageId);
+			oneOf(database).updateLastSentTime(txn, contactId, messageId1);
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -974,12 +970,10 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			will(returnValue(ids));
 			oneOf(database).getRawMessage(txn, messageId);
 			will(returnValue(raw));
-			oneOf(database).updateExpiryTime(txn, contactId, messageId,
-					maxLatency);
+			oneOf(database).updateLastSentTime(txn, contactId, messageId);
 			oneOf(database).getRawMessage(txn, messageId1);
 			will(returnValue(raw1));
-			oneOf(database).updateExpiryTime(txn, contactId, messageId1,
-					maxLatency);
+			oneOf(database).updateLastSentTime(txn, contactId, messageId1);
 			oneOf(database).lowerRequestedFlag(txn, contactId, ids);
 			oneOf(database).commitTransaction(txn);
 			oneOf(eventBus).broadcast(with(any(MessagesSentEvent.class)));
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
index e46a75b68460ba81166b20aabbfcddcbca1fe4b8..9fb1fd07c451edd579024d3e3af093c917887924 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
@@ -96,6 +96,11 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 	 */
 	private static final int STEADY_STATE_BLOCKS = 5;
 
+	/**
+	 * Max latency for the database
+	 */
+	private static final int MAX_LATENCY = 1000;
+
 	protected final File testDir = getTestDirectory();
 	private final File resultsFile = new File(getTestName() + ".tsv");
 	protected final Random random = new Random();
@@ -448,7 +453,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 		benchmark(name, db -> {
 			Connection txn = db.startTransaction();
 			db.getMessagesToOffer(txn, pickRandom(contacts).getId(),
-					MAX_MESSAGE_IDS);
+					MAX_MESSAGE_IDS, MAX_LATENCY);
 			db.commitTransaction(txn);
 		});
 	}
@@ -470,7 +475,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 		benchmark(name, db -> {
 			Connection txn = db.startTransaction();
 			db.getMessagesToSend(txn, pickRandom(contacts).getId(),
-					MAX_MESSAGE_IDS);
+					MAX_MESSAGE_IDS, MAX_LATENCY);
 			db.commitTransaction(txn);
 		});
 	}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
index e88038258ad7007b909eb499cc5243eaee11d398..68725cabdea6e6d114b61cafe97582d113cc74a5 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -57,6 +57,7 @@ import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERE
 import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
+import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
 import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getClientId;
 import static org.briarproject.bramble.test.TestUtils.getGroup;
@@ -79,6 +80,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 	private static final int ONE_MEGABYTE = 1024 * 1024;
 	private static final int MAX_SIZE = 5 * ONE_MEGABYTE;
+	private static final int MAX_LATENCY = 1000;
 
 	private final SecretKey key = getSecretKey();
 	private final File testDir = getTestDirectory();
@@ -202,16 +204,16 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The contact has not seen the message, so it should be sendable
 		Collection<MessageId> ids =
-				db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+				db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
 
 		// Changing the status to seen = true should make the message unsendable
 		db.raiseSeenFlag(txn, contactId, messageId);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
@@ -233,30 +235,30 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The message has not been validated, so it should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
+				ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Marking the message delivered should make it sendable
 		db.setMessageState(txn, messageId, DELIVERED);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
 
 		// Marking the message invalid should make it unsendable
 		db.setMessageState(txn, messageId, INVALID);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Marking the message pending should make it unsendable
 		db.setMessageState(txn, messageId, PENDING);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
@@ -277,37 +279,37 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The group is invisible, so the message should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
+				ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Making the group visible should not make the message sendable
 		db.addGroupVisibility(txn, contactId, groupId, false);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Sharing the group should make the message sendable
 		db.setGroupVisibility(txn, contactId, groupId, true);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
 
 		// Unsharing the group should make the message unsendable
 		db.setGroupVisibility(txn, contactId, groupId, false);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Making the group invisible should make the message unsendable
 		db.removeGroupVisibility(txn, contactId, groupId);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
@@ -329,16 +331,16 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The message is not shared, so it should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
+				ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Sharing the message should make it sendable
 		db.setMessageShared(txn, messageId);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
 
 		db.commitTransaction(txn);
@@ -360,11 +362,11 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The message is sendable, but too large to send
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				size - 1);
+				size - 1, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// The message is just the right size to send
-		ids = db.getMessagesToSend(txn, contactId, size);
+		ids = db.getMessagesToSend(txn, contactId, size, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
 
 		db.commitTransaction(txn);
@@ -427,25 +429,66 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Retrieve the message from the database and mark it as sent
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
+				ONE_MEGABYTE, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
-		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
+		db.updateLastSentTime(txn, contactId, messageId);
 
 		// The message should no longer be sendable
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Pretend that the message was acked
 		db.raiseSeenFlag(txn, contactId, messageId);
 
 		// The message still should not be sendable
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
 		db.close();
 	}
 
+	@Test
+	public void testFasterMessageRetransmission() throws Exception {
+		long now = System.currentTimeMillis();
+		long steps[] = {now, now, calculateExpiry(now, MAX_LATENCY / 2, 1),
+				calculateExpiry(now, MAX_LATENCY / 2, 1)};
+		Database<Connection> db =
+				open(false, new ArrayClock(steps));
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
+
+		// Retrieve the message from the database and mark it as sent
+		// Time: now
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE, MAX_LATENCY);
+		assertEquals(singletonList(messageId), ids);
+		// Time: now
+		db.updateLastSentTime(txn, contactId, messageId);
+
+		// The message should no longer be sendable via transports with
+		// latencies >= MAX_LATENCY
+		// Time: now + MAX_LATENCY * 2
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
+		assertTrue(ids.isEmpty());
+
+		// The message should be sendable via a transport with lower latency
+		// Time: now + MAX_LATENCY * 2
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE,
+				MAX_LATENCY / 4);
+		assertEquals(singletonList(messageId), ids);
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testGetFreeSpace() throws Exception {
 		byte[] largeBody = new byte[MAX_MESSAGE_LENGTH];
@@ -1530,7 +1573,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		assertFalse(status.isSeen());
 
 		// Pretend the message was sent to the contact
-		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
+		db.updateLastSentTime(txn, contactId, messageId);
 
 		// The message should be sent but not seen
 		status = db.getMessageStatus(txn, contactId, messageId);
@@ -1649,9 +1692,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The message should be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
+				ONE_MEGABYTE, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
 
 		// The raw message should not be null
@@ -1664,9 +1707,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
 
 		// The message should not be sendable
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
+		ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// The raw message should be null
@@ -1744,37 +1787,43 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		db.addMessage(txn, message, UNKNOWN, false, null);
 
 		// There should be no messages to send
-		assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
+		assertEquals(Long.MAX_VALUE,
+				db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
 		// Share the group with the contact - still no messages to send
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
+		assertEquals(Long.MAX_VALUE,
+				db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
 		// Set the message's state to DELIVERED - still no messages to send
 		db.setMessageState(txn, messageId, DELIVERED);
-		assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
+		assertEquals(Long.MAX_VALUE,
+				db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
 		// Share the message - now it should be sendable immediately
 		db.setMessageShared(txn, messageId);
-		assertEquals(0, db.getNextSendTime(txn, contactId));
+		assertEquals(0, db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
 		// Mark the message as requested - it should still be sendable
 		db.raiseRequestedFlag(txn, contactId, messageId);
-		assertEquals(0, db.getNextSendTime(txn, contactId));
+		assertEquals(0, db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
-		// Update the message's expiry time as though we sent it - now the
+		// Update the message's lastSentTime as though we sent it - now the
 		// message should be sendable after one round-trip
-		db.updateExpiryTime(txn, contactId, messageId, 1000);
-		assertEquals(now + 2000, db.getNextSendTime(txn, contactId));
+		db.updateLastSentTime(txn, contactId, messageId);
+		assertEquals(calculateExpiry(now, MAX_LATENCY, 1),
+				db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
 		// Update the message's expiry time again - now it should be sendable
 		// after two round-trips
-		db.updateExpiryTime(txn, contactId, messageId, 1000);
-		assertEquals(now + 4000, db.getNextSendTime(txn, contactId));
+		db.updateLastSentTime(txn, contactId, messageId);
+		assertEquals(calculateExpiry(now, MAX_LATENCY, 2),
+				db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
 		// Delete the message - there should be no messages to send
 		db.deleteMessage(txn, messageId);
-		assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
+		assertEquals(Long.MAX_VALUE,
+				db.getNextSendTime(txn, contactId, MAX_LATENCY));
 
 		db.commitTransaction(txn);
 		db.close();
@@ -1869,4 +1918,24 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 			Thread.sleep(milliseconds);
 		}
 	}
+
+	private static class ArrayClock implements Clock {
+
+		private final long[] times;
+		private int index = 0;
+
+		private ArrayClock(long... times) {
+			this.times = times;
+		}
+
+		@Override
+		public long currentTimeMillis() {
+			return times[index++];
+		}
+
+		@Override
+		public void sleep(long milliseconds) throws InterruptedException {
+			Thread.sleep(milliseconds);
+		}
+	}
 }