diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index fa6226172873d9530ce8ecc73482390410cd0b27..6e5a890629c5f98ea8005fd60a70580d0d85f521 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -220,6 +220,13 @@ interface Database<T> {
 	boolean containsVisibleMessage(T txn, ContactId c, MessageId m)
 			throws DbException;
 
+	/**
+	 * Returns the number of messages offered by the given contact.
+	 * <p>
+	 * Locking: message read.
+	 */
+	int countOfferedMessages(T txn, ContactId c) throws DbException;
+
 	/**
 	 * Returns the status of all groups to which the user subscribes or can
 	 * subscribe, excluding inbox groups.
@@ -634,8 +641,8 @@ interface Database<T> {
 	void removeMessage(T txn, MessageId m) throws DbException;
 
 	/**
-	 * Removes an offered message ID that was offered by the given contact, or
-	 * returns false if there is no such message ID.
+	 * Removes an offered message that was offered by the given contact, or
+	 * returns false if there is no such message.
 	 * <p>
 	 * Locking: message write.
 	 */
@@ -643,7 +650,7 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Removes the given offered message IDs that were offered by the given
+	 * Removes the given offered messages that were offered by the given
 	 * contact.
 	 * <p>
 	 * Locking: message write.
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index 66ad996d7ae13587dc9a92c372d363122e89583d..79f39e86c900f23765a4e5300f8ecbcdbb4d7f14 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -6,6 +6,7 @@ import static net.sf.briar.db.DatabaseConstants.BYTES_PER_SWEEP;
 import static net.sf.briar.db.DatabaseConstants.CRITICAL_FREE_SPACE;
 import static net.sf.briar.db.DatabaseConstants.MAX_BYTES_BETWEEN_SPACE_CHECKS;
 import static net.sf.briar.db.DatabaseConstants.MAX_MS_BETWEEN_SPACE_CHECKS;
+import static net.sf.briar.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 import static net.sf.briar.db.DatabaseConstants.MIN_FREE_SPACE;
 
 import java.io.IOException;
@@ -1363,14 +1364,16 @@ DatabaseCleaner.Callback {
 					try {
 						if(!db.containsContact(txn, c))
 							throw new NoSuchContactException();
+						int count = db.countOfferedMessages(txn, c);
 						for(MessageId m : o.getMessageIds()) {
 							if(db.containsVisibleMessage(txn, c, m)) {
 								db.raiseSeenFlag(txn, c, m);
 								db.raiseAckFlag(txn, c, m);
 								ack = true;
-							} else {
+							} else if(count < MAX_OFFERED_MESSAGES) {
 								db.addOfferedMessage(txn, c, m);
 								request = true;
+								count++;
 							}
 						}
 						db.commitTransaction(txn);
diff --git a/briar-core/src/net/sf/briar/db/DatabaseConstants.java b/briar-core/src/net/sf/briar/db/DatabaseConstants.java
index 4a8a5dfeb5b037b2dc74549a63c8f0b74e65d4ed..53a55111091004735daa7dbb6aad7382f9039ff1 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseConstants.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseConstants.java
@@ -2,6 +2,13 @@ package net.sf.briar.db;
 
 interface DatabaseConstants {
 
+	/**
+	 * The maximum number of offered messages from each contact that will be
+	 * stored. If offers arrive more quickly than requests can be sent and this
+	 * limit is reached, additional offers will not be stored.
+	 */
+	int MAX_OFFERED_MESSAGES = 1000;
+
 	// FIXME: These should be configurable
 
 	/**
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index a962f87ffbcc1981cd91d512a0b86befe28d0898..a896416c9895ccdd9b5a84d11f5bf046d30e5438 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -668,10 +668,20 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean addGroup(Connection txn, Group g) throws DbException {
-		if(maximumSubscriptionsReached(txn)) return false;
 		PreparedStatement ps = null;
+		ResultSet rs = null;
 		try {
-			String sql = "INSERT INTO groups"
+			String sql = "SELECT COUNT (groupId) FROM groups";
+			ps = txn.prepareStatement(sql);
+			rs = ps.executeQuery();
+			if(!rs.next()) throw new DbStateException();
+			int count = rs.getInt(1);
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			if(count > MAX_SUBSCRIPTIONS) throw new DbStateException();
+			if(count == MAX_SUBSCRIPTIONS) return false;
+			sql = "INSERT INTO groups"
 					+ " (groupId, name, salt, visibleToAll)"
 					+ " VALUES (?, ?, ?, FALSE)";
 			ps = txn.prepareStatement(sql);
@@ -682,27 +692,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(affected != 1) throw new DbStateException();
 			ps.close();
 			return true;
-		} catch(SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	private boolean maximumSubscriptionsReached(Connection txn)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT COUNT (NULL) FROM groups";
-			ps = txn.prepareStatement(sql);
-			rs = ps.executeQuery();
-			if(!rs.next()) throw new DbStateException();
-			int count = rs.getInt(1);
-			if(rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			if(count > MAX_SUBSCRIPTIONS) throw new DbStateException();
-			return count == MAX_SUBSCRIPTIONS;
 		} catch(SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -772,7 +761,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	// FIXME: Limit the number of offers per contact
 	public void addOfferedMessage(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1130,6 +1118,29 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public int countOfferedMessages(Connection txn, ContactId c)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT COUNT (messageId) FROM offers "
+					+ " WHERE contactId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			rs = ps.executeQuery();
+			if(!rs.next()) throw new DbException();
+			int count = rs.getInt(1);
+			if(rs.next()) throw new DbException();
+			rs.close();
+			ps.close();
+			return count;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<GroupStatus> getAvailableGroups(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index ff8a65a2bc0f7f316f8b326d304112e1ec75dfe8..d6311265f2bfcb04ce5cf06c2457e28da4bdb3cf 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -2,9 +2,9 @@ package net.sf.briar.db;
 
 import static net.sf.briar.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static net.sf.briar.api.messaging.MessagingConstants.GROUP_SALT_LENGTH;
+import static net.sf.briar.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 
 import java.util.Arrays;
-import java.util.BitSet;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -1087,9 +1087,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	public void testReceiveOffer() throws Exception {
 		final MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		final MessageId messageId2 = new MessageId(TestUtils.getRandomId());
-		final BitSet expectedRequest = new BitSet(3);
-		expectedRequest.set(0);
-		expectedRequest.set(2);
+		final MessageId messageId3 = new MessageId(TestUtils.getRandomId());
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1101,16 +1099,25 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
+			// There's room for two more offered messages
+			oneOf(database).countOfferedMessages(txn, contactId);
+			will(returnValue(MAX_OFFERED_MESSAGES - 2));
+			// The first message isn't visible - request it
 			oneOf(database).containsVisibleMessage(txn, contactId, messageId);
-			will(returnValue(false)); // Not visible - request message # 0
+			will(returnValue(false));
 			oneOf(database).addOfferedMessage(txn, contactId, messageId);
+			// The second message is visible - ack it
 			oneOf(database).containsVisibleMessage(txn, contactId, messageId1);
-			will(returnValue(true)); // Visible - ack message # 1
+			will(returnValue(true));
 			oneOf(database).raiseSeenFlag(txn, contactId, messageId1);
 			oneOf(database).raiseAckFlag(txn, contactId, messageId1);
+			// The third message isn't visible - request it
 			oneOf(database).containsVisibleMessage(txn, contactId, messageId2);
-			will(returnValue(false)); // Not visible - request message # 2
+			will(returnValue(false));
 			oneOf(database).addOfferedMessage(txn, contactId, messageId2);
+			// The fourth message isn't visible, but there's no room to store it
+			oneOf(database).containsVisibleMessage(txn, contactId, messageId3);
+			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 			oneOf(listener).eventOccurred(with(any(MessageToAckEvent.class)));
 			oneOf(listener).eventOccurred(with(any(
@@ -1120,7 +1127,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				shutdown);
 
 		db.addListener(listener);
-		Offer o = new Offer(Arrays.asList(messageId, messageId1, messageId2));
+		Offer o = new Offer(Arrays.asList(messageId, messageId1, messageId2,
+				messageId3));
 		db.receiveOffer(contactId, o);
 		context.assertIsSatisfied();
 	}
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index b7b6b5c29c722049491112ab94dc9558d99892e4..29d3381e0796b94126f0a627ac37aa1959a80b9f 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -1509,6 +1509,35 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testOfferedMessages() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact - initially there should be no offered messages
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
+		assertEquals(0, db.countOfferedMessages(txn, contactId));
+
+		// Add some offered messages and count them
+		List<MessageId> ids = new ArrayList<MessageId>();
+		for(int i = 0; i < 10; i++) {
+			MessageId m = new MessageId(TestUtils.getRandomId());
+			db.addOfferedMessage(txn, contactId, m);
+			ids.add(m);
+		}
+		assertEquals(10, db.countOfferedMessages(txn, contactId));
+
+		// Remove some of the offered messages and count again
+		List<MessageId> half = ids.subList(0, 5);
+		db.removeOfferedMessages(txn, contactId, half);
+		assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5)));
+		assertEquals(4, db.countOfferedMessages(txn, contactId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testExceptionHandling() throws Exception {
 		Database<Connection> db = open(false);