diff --git a/api/net/sf/briar/api/protocol/Ack.java b/api/net/sf/briar/api/protocol/Ack.java
index 53feeb754e6374625e64393e1bedea7e832588e1..8be9162b23ff254918ff4be6a78d4ed618410851 100644
--- a/api/net/sf/briar/api/protocol/Ack.java
+++ b/api/net/sf/briar/api/protocol/Ack.java
@@ -5,9 +5,6 @@ import java.util.Collection;
 /** A packet acknowledging receipt of one or more batches. */
 public interface Ack {
 
-	/** The maximum number of batch IDs per ack. */
-	static final int MAX_IDS_PER_ACK = 29959;
-
 	/** Returns the IDs of the acknowledged batches. */
 	Collection<BatchId> getBatchIds();
 }
diff --git a/api/net/sf/briar/api/protocol/Offer.java b/api/net/sf/briar/api/protocol/Offer.java
index 6ff90d833997557e1f755f39d6ba33fb0492962e..0d12875ca5fe5c2306120849cbf9d005b6f43369 100644
--- a/api/net/sf/briar/api/protocol/Offer.java
+++ b/api/net/sf/briar/api/protocol/Offer.java
@@ -5,9 +5,6 @@ import java.util.Collection;
 /** A packet offering the recipient some messages. */
 public interface Offer {
 
-	/** The maximum number of message IDs per offer. */
-	static final int MAX_IDS_PER_OFFER = 29959;
-
 	/** Returns the message IDs contained in the offer. */
 	Collection<MessageId> getMessageIds();
 }
diff --git a/api/net/sf/briar/api/protocol/SubscriptionUpdate.java b/api/net/sf/briar/api/protocol/SubscriptionUpdate.java
index 2124a72e2df8577c9c09ed9bdf38c713062c9e15..31e2f4672f4b31b303700da7d479d8c0fd150d1d 100644
--- a/api/net/sf/briar/api/protocol/SubscriptionUpdate.java
+++ b/api/net/sf/briar/api/protocol/SubscriptionUpdate.java
@@ -6,7 +6,7 @@ import java.util.Map;
 public interface SubscriptionUpdate {
 
 	/** The maximum number of subscriptions per update. */
-	static final int MAX_SUBS_PER_UPDATE = 6393;
+	static final int MAX_SUBS_PER_UPDATE = 6000;
 
 	/** Returns the subscriptions contained in the update. */
 	Map<Group, Long> getSubscriptions();
diff --git a/api/net/sf/briar/api/serial/SerialComponent.java b/api/net/sf/briar/api/serial/SerialComponent.java
new file mode 100644
index 0000000000000000000000000000000000000000..1dc84673202278bfb42c2cf38fc7042ce0fa014f
--- /dev/null
+++ b/api/net/sf/briar/api/serial/SerialComponent.java
@@ -0,0 +1,12 @@
+package net.sf.briar.api.serial;
+
+public interface SerialComponent {
+
+	int getSerialisedListEndLength();
+
+	int getSerialisedListStartLength();
+
+	int getSerialisedUniqueIdLength(int id);
+
+	int getSerialisedUserDefinedIdLength(int id);
+}
diff --git a/components/net/sf/briar/protocol/AckReader.java b/components/net/sf/briar/protocol/AckReader.java
index 5f78511ae7fdfd78b1cb97ad8d5e5ed9908900c2..af02456d4265197733832dd814e1f0ee44b8bfc4 100644
--- a/components/net/sf/briar/protocol/AckReader.java
+++ b/components/net/sf/briar/protocol/AckReader.java
@@ -3,7 +3,6 @@ package net.sf.briar.protocol;
 import java.io.IOException;
 import java.util.Collection;
 
-import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.ProtocolConstants;
@@ -31,7 +30,6 @@ class AckReader implements ObjectReader<Ack> {
 		r.readUserDefinedId(Types.ACK);
 		r.addObjectReader(Types.BATCH_ID, batchIdReader);
 		Collection<BatchId> batches = r.readList(BatchId.class);
-		if(batches.size() > Ack.MAX_IDS_PER_ACK) throw new FormatException();
 		r.removeObjectReader(Types.BATCH_ID);
 		r.removeConsumer(counting);
 		// Build and return the ack
diff --git a/components/net/sf/briar/protocol/OfferReader.java b/components/net/sf/briar/protocol/OfferReader.java
index 04a15536cf44ab3340d6b7900a535b676e68181c..7d16c0b2f3c377e3886e9592548faba666b6a762 100644
--- a/components/net/sf/briar/protocol/OfferReader.java
+++ b/components/net/sf/briar/protocol/OfferReader.java
@@ -3,7 +3,6 @@ package net.sf.briar.protocol;
 import java.io.IOException;
 import java.util.Collection;
 
-import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolConstants;
@@ -32,8 +31,6 @@ class OfferReader implements ObjectReader<Offer> {
 		r.readUserDefinedId(Types.OFFER);
 		r.addObjectReader(Types.MESSAGE_ID, messageIdReader);
 		Collection<MessageId> messages = r.readList(MessageId.class);
-		if(messages.size() > Offer.MAX_IDS_PER_OFFER)
-			throw new FormatException();
 		r.removeObjectReader(Types.MESSAGE_ID);
 		r.removeConsumer(counting);
 		// Build and return the offer
diff --git a/components/net/sf/briar/protocol/writers/AckWriterImpl.java b/components/net/sf/briar/protocol/writers/AckWriterImpl.java
index 886455753d280e209b9c578035c2f7ef3eb831c6..a3babc533eb44d6c4ee32681ba906c38081d3f1e 100644
--- a/components/net/sf/briar/protocol/writers/AckWriterImpl.java
+++ b/components/net/sf/briar/protocol/writers/AckWriterImpl.java
@@ -3,46 +3,53 @@ package net.sf.briar.protocol.writers;
 import java.io.IOException;
 import java.io.OutputStream;
 
-import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.protocol.writers.AckWriter;
+import net.sf.briar.api.serial.SerialComponent;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 
 class AckWriterImpl implements AckWriter {
 
 	private final OutputStream out;
+	private final SerialComponent serial;
 	private final Writer w;
 
 	private boolean started = false;
-	private int idsWritten = 0;
+	private int capacity = ProtocolConstants.MAX_PACKET_LENGTH; // FIXME
 
-	AckWriterImpl(OutputStream out, WriterFactory writerFactory) {
+	AckWriterImpl(OutputStream out, SerialComponent serial,
+			WriterFactory writerFactory) {
 		this.out = out;
+		this.serial = serial;
 		w = writerFactory.createWriter(out);
 	}
 
 	public boolean writeBatchId(BatchId b) throws IOException {
-		if(!started) {
-			w.writeUserDefinedTag(Types.ACK);
-			w.writeListStart();
-			started = true;
-		}
-		if(idsWritten >= Ack.MAX_IDS_PER_ACK) return false;
+		if(!started) start();
+		int length = serial.getSerialisedUniqueIdLength(Types.BATCH_ID);
+		if(capacity < length + serial.getSerialisedListEndLength())
+			return false;
 		b.writeTo(w);
-		idsWritten++;
+		capacity -= length;
 		return true;
 	}
 
 	public void finish() throws IOException {
-		if(!started) {
-			w.writeUserDefinedTag(Types.ACK);
-			w.writeListStart();
-			started = true;
-		}
+		if(!started) start();
 		w.writeListEnd();
 		out.flush();
+		capacity = ProtocolConstants.MAX_PACKET_LENGTH; // FIXME
 		started = false;
 	}
+
+	private void start() throws IOException {
+		w.writeUserDefinedTag(Types.ACK);
+		capacity -= serial.getSerialisedUserDefinedIdLength(Types.ACK);
+		w.writeListStart();
+		capacity -= serial.getSerialisedListStartLength();
+		started = true;
+	}
 }
diff --git a/components/net/sf/briar/protocol/writers/BatchWriterImpl.java b/components/net/sf/briar/protocol/writers/BatchWriterImpl.java
index fac083baff88986a3979aa29d3300d3b52292a34..99e668bc5155d0d6584abcac4cfeb1312f61c1df 100644
--- a/components/net/sf/briar/protocol/writers/BatchWriterImpl.java
+++ b/components/net/sf/briar/protocol/writers/BatchWriterImpl.java
@@ -9,50 +9,53 @@ import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.protocol.writers.BatchWriter;
+import net.sf.briar.api.serial.SerialComponent;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 
 class BatchWriterImpl implements BatchWriter {
 
 	private final DigestOutputStream out;
+	private final SerialComponent serial;
 	private final Writer w;
 	private final MessageDigest messageDigest;
 
 	private boolean started = false;
+	private int capacity = ProtocolConstants.MAX_PACKET_LENGTH; // FIXME
 
-	BatchWriterImpl(OutputStream out, WriterFactory writerFactory,
-			MessageDigest messageDigest) {
+	BatchWriterImpl(OutputStream out, SerialComponent serial,
+			WriterFactory writerFactory, MessageDigest messageDigest) {
 		this.out = new DigestOutputStream(out, messageDigest);
+		this.serial = serial;
 		w = writerFactory.createWriter(this.out);
 		this.messageDigest = messageDigest;
 	}
 
 	public boolean writeMessage(byte[] message) throws IOException {
-		if(!started) {
-			messageDigest.reset();
-			w.writeUserDefinedTag(Types.BATCH);
-			w.writeListStart();
-			started = true;
-		}
-		// Allow one byte for the list end tag
-		int capacity = ProtocolConstants.MAX_PACKET_LENGTH
-		- (int) w.getBytesWritten() - 1;
-		if(capacity < message.length) return false;
-		// Bypass the writer and write each raw message directly
+		if(!started) start();
+		if(capacity < message.length + serial.getSerialisedListEndLength())
+			return false;
+		// Bypass the writer and write the raw message directly
 		out.write(message);
+		capacity -= message.length;
 		return true;
 	}
 
 	public BatchId finish() throws IOException {
-		if(!started) {
-			messageDigest.reset();
-			w.writeUserDefinedTag(Types.BATCH);
-			w.writeListStart();
-			started = true;
-		}
+		if(!started) start();
 		w.writeListEnd();
 		out.flush();
+		capacity = ProtocolConstants.MAX_PACKET_LENGTH; // FIXME
 		started = false;
 		return new BatchId(messageDigest.digest());
 	}
+
+	private void start() throws IOException {
+		messageDigest.reset();
+		w.writeUserDefinedTag(Types.BATCH);
+		capacity -= serial.getSerialisedUserDefinedIdLength(Types.BATCH);
+		w.writeListStart();
+		capacity -= serial.getSerialisedListStartLength();
+		started = true;
+	}
 }
diff --git a/components/net/sf/briar/protocol/writers/OfferWriterImpl.java b/components/net/sf/briar/protocol/writers/OfferWriterImpl.java
index c9f0670da4f1359bc24c1a42495c30b84be2e5c6..13baf0258567c5d5b66e94df16e788c47064f2af 100644
--- a/components/net/sf/briar/protocol/writers/OfferWriterImpl.java
+++ b/components/net/sf/briar/protocol/writers/OfferWriterImpl.java
@@ -4,45 +4,50 @@ import java.io.IOException;
 import java.io.OutputStream;
 
 import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.Offer;
+import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.protocol.writers.OfferWriter;
+import net.sf.briar.api.serial.SerialComponent;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 
 class OfferWriterImpl implements OfferWriter {
 
 	private final OutputStream out;
+	private final SerialComponent serial;
 	private final Writer w;
 
 	private boolean started = false;
-	private int idsWritten = 0;
+	private int capacity = ProtocolConstants.MAX_PACKET_LENGTH; // FIXME
 
-	OfferWriterImpl(OutputStream out, WriterFactory writerFactory) {
+	OfferWriterImpl(OutputStream out, SerialComponent serial,
+			WriterFactory writerFactory) {
 		this.out = out;
+		this.serial = serial;
 		w = writerFactory.createWriter(out);
 	}
 
 	public boolean writeMessageId(MessageId m) throws IOException {
-		if(!started) {
-			w.writeUserDefinedTag(Types.OFFER);
-			w.writeListStart();
-			started = true;
-		}
-		if(idsWritten >= Offer.MAX_IDS_PER_OFFER) return false;
+		if(!started) start();
+		int length = serial.getSerialisedUniqueIdLength(Types.MESSAGE_ID);
+		if(capacity <  length + serial.getSerialisedListEndLength())
+			return false;
 		m.writeTo(w);
-		idsWritten++;
+		capacity -= length;
 		return true;
 	}
 
 	public void finish() throws IOException {
-		if(!started) {
-			w.writeUserDefinedTag(Types.OFFER);
-			w.writeListStart();
-			started = true;
-		}
+		if(!started) start();
 		w.writeListEnd();
 		out.flush();
+		capacity = ProtocolConstants.MAX_PACKET_LENGTH; // FIXME
 		started = false;
 	}
+
+	private void start() throws IOException {
+		w.writeUserDefinedTag(Types.OFFER);
+		w.writeListStart();
+		started = true;
+	}
 }
diff --git a/components/net/sf/briar/protocol/writers/ProtocolWriterFactoryImpl.java b/components/net/sf/briar/protocol/writers/ProtocolWriterFactoryImpl.java
index 25999403679a4406a1246f38cb61ebd77d4035c9..b0e36477993344b6b118ab78e82dbe272deca18d 100644
--- a/components/net/sf/briar/protocol/writers/ProtocolWriterFactoryImpl.java
+++ b/components/net/sf/briar/protocol/writers/ProtocolWriterFactoryImpl.java
@@ -11,6 +11,7 @@ import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.writers.RequestWriter;
 import net.sf.briar.api.protocol.writers.SubscriptionWriter;
 import net.sf.briar.api.protocol.writers.TransportWriter;
+import net.sf.briar.api.serial.SerialComponent;
 import net.sf.briar.api.serial.WriterFactory;
 
 import com.google.inject.Inject;
@@ -18,25 +19,27 @@ import com.google.inject.Inject;
 class ProtocolWriterFactoryImpl implements ProtocolWriterFactory {
 
 	private final MessageDigest messageDigest;
+	private final SerialComponent serial;
 	private final WriterFactory writerFactory;
 
 	@Inject
 	ProtocolWriterFactoryImpl(CryptoComponent crypto,
-			WriterFactory writerFactory) {
+			SerialComponent serial, WriterFactory writerFactory) {
 		messageDigest = crypto.getMessageDigest();
+		this.serial = serial;
 		this.writerFactory = writerFactory;
 	}
 
 	public AckWriter createAckWriter(OutputStream out) {
-		return new AckWriterImpl(out, writerFactory);
+		return new AckWriterImpl(out, serial, writerFactory);
 	}
 
 	public BatchWriter createBatchWriter(OutputStream out) {
-		return new BatchWriterImpl(out, writerFactory, messageDigest);
+		return new BatchWriterImpl(out, serial, writerFactory, messageDigest);
 	}
 
 	public OfferWriter createOfferWriter(OutputStream out) {
-		return new OfferWriterImpl(out, writerFactory);
+		return new OfferWriterImpl(out, serial, writerFactory);
 	}
 
 	public RequestWriter createRequestWriter(OutputStream out) {
diff --git a/components/net/sf/briar/serial/SerialComponentImpl.java b/components/net/sf/briar/serial/SerialComponentImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..d256294e2bfe9cee30b1660ea410e8ccd5dfc6fa
--- /dev/null
+++ b/components/net/sf/briar/serial/SerialComponentImpl.java
@@ -0,0 +1,34 @@
+package net.sf.briar.serial;
+
+import net.sf.briar.api.protocol.UniqueId;
+import net.sf.briar.api.serial.SerialComponent;
+
+class SerialComponentImpl implements SerialComponent {
+
+	public int getSerialisedListEndLength() {
+		return 1;
+	}
+
+	public int getSerialisedListStartLength() {
+		return 1;
+	}
+
+	public int getSerialisedUniqueIdLength(int id) {
+		// User-defined ID, BYTES tag, length spec, bytes
+		return getSerialisedUserDefinedIdLength(id) + 1 +
+		getSerialisedLengthSpecLength(UniqueId.LENGTH) +
+		UniqueId.LENGTH;
+	}
+
+	private int getSerialisedLengthSpecLength(int length) {
+		assert length >= 0;
+		if(length < 128) return 1; // Uint7
+		if(length < Short.MAX_VALUE) return 3; // Int16
+		return 5; // Int32
+	}
+
+	public int getSerialisedUserDefinedIdLength(int id) {
+		assert id >= 0 && id <= 255;
+		return id < 32 ? 1 : 2;
+	}
+}
diff --git a/components/net/sf/briar/serial/SerialModule.java b/components/net/sf/briar/serial/SerialModule.java
index 48fb7b1a222049664611f69600ac44caad2fbd98..667087076b365b859b2f05b85272c3fb09434239 100644
--- a/components/net/sf/briar/serial/SerialModule.java
+++ b/components/net/sf/briar/serial/SerialModule.java
@@ -1,15 +1,19 @@
 package net.sf.briar.serial;
 
 import net.sf.briar.api.serial.ReaderFactory;
+import net.sf.briar.api.serial.SerialComponent;
 import net.sf.briar.api.serial.WriterFactory;
 
 import com.google.inject.AbstractModule;
+import com.google.inject.Singleton;
 
 public class SerialModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
 		bind(ReaderFactory.class).to(ReaderFactoryImpl.class);
+		bind(SerialComponent.class).to(SerialComponentImpl.class).in(
+				Singleton.class);
 		bind(WriterFactory.class).to(WriterFactoryImpl.class);
 	}
 }
diff --git a/test/net/sf/briar/protocol/writers/ConstantsTest.java b/test/net/sf/briar/protocol/writers/ConstantsTest.java
index 6d3781433758f81effc5cf3f5f3b3f1b7f5ee9ce..6115e08af7152d8ed885fdbbd1d9bd312dca4678 100644
--- a/test/net/sf/briar/protocol/writers/ConstantsTest.java
+++ b/test/net/sf/briar/protocol/writers/ConstantsTest.java
@@ -8,7 +8,6 @@ import java.util.Map;
 import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorFactory;
 import net.sf.briar.api.protocol.BatchId;
@@ -17,7 +16,6 @@ import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportUpdate;
@@ -27,6 +25,7 @@ import net.sf.briar.api.protocol.writers.BatchWriter;
 import net.sf.briar.api.protocol.writers.OfferWriter;
 import net.sf.briar.api.protocol.writers.SubscriptionWriter;
 import net.sf.briar.api.protocol.writers.TransportWriter;
+import net.sf.briar.api.serial.SerialComponent;
 import net.sf.briar.api.serial.WriterFactory;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.protocol.ProtocolModule;
@@ -41,6 +40,7 @@ public class ConstantsTest extends TestCase {
 
 	private final WriterFactory writerFactory;
 	private final CryptoComponent crypto;
+	private final SerialComponent serial;
 	private final GroupFactory groupFactory;
 	private final AuthorFactory authorFactory;
 	private final MessageEncoder messageEncoder;
@@ -51,6 +51,7 @@ public class ConstantsTest extends TestCase {
 				new ProtocolModule(), new SerialModule());
 		writerFactory = i.getInstance(WriterFactory.class);
 		crypto = i.getInstance(CryptoComponent.class);
+		serial = i.getInstance(SerialComponent.class);
 		groupFactory = i.getInstance(GroupFactory.class);
 		authorFactory = i.getInstance(AuthorFactory.class);
 		messageEncoder = i.getInstance(MessageEncoder.class);
@@ -61,15 +62,10 @@ public class ConstantsTest extends TestCase {
 		// Create an ack with the maximum number of batch IDs
 		ByteArrayOutputStream out = new ByteArrayOutputStream(
 				ProtocolConstants.MAX_PACKET_LENGTH);
-		AckWriter a = new AckWriterImpl(out, writerFactory);
-		for(int i = 0; i < Ack.MAX_IDS_PER_ACK; i++) {
-			assertTrue(a.writeBatchId(new BatchId(TestUtils.getRandomId())));
-		}
-		// Check that no more batch IDs can be written
-		assertFalse(a.writeBatchId(new BatchId(TestUtils.getRandomId())));
+		AckWriter a = new AckWriterImpl(out, serial, writerFactory);
+		while(a.writeBatchId(new BatchId(TestUtils.getRandomId())));
 		a.finish();
 		// Check the size of the serialised ack
-		assertTrue(out.size() > UniqueId.LENGTH * Ack.MAX_IDS_PER_ACK);
 		assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
 	}
 
@@ -92,7 +88,7 @@ public class ConstantsTest extends TestCase {
 		// Add the message to a batch
 		ByteArrayOutputStream out = new ByteArrayOutputStream(
 				ProtocolConstants.MAX_PACKET_LENGTH);
-		BatchWriter b = new BatchWriterImpl(out, writerFactory,
+		BatchWriter b = new BatchWriterImpl(out, serial, writerFactory,
 				crypto.getMessageDigest());
 		b.writeMessage(message.getBytes());
 		b.finish();
@@ -108,16 +104,10 @@ public class ConstantsTest extends TestCase {
 		// Create an offer with the maximum number of message IDs
 		ByteArrayOutputStream out = new ByteArrayOutputStream(
 				ProtocolConstants.MAX_PACKET_LENGTH);
-		OfferWriter o = new OfferWriterImpl(out, writerFactory);
-		for(int i = 0; i < Offer.MAX_IDS_PER_OFFER; i++) {
-			assertTrue(o.writeMessageId(new MessageId(
-					TestUtils.getRandomId())));
-		}
-		// Check that no more message IDs can be written
-		assertFalse(o.writeMessageId(new MessageId(TestUtils.getRandomId())));
+		OfferWriter o = new OfferWriterImpl(out, serial, writerFactory);
+		while(o.writeMessageId(new MessageId(TestUtils.getRandomId())));
 		o.finish();
 		// Check the size of the serialised offer
-		assertTrue(out.size() > UniqueId.LENGTH * Offer.MAX_IDS_PER_OFFER);
 		assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
 	}