diff --git a/Nibriboard/Client/LineIncubator.cs b/Nibriboard/Client/LineIncubator.cs
index 77ee022..1116b34 100644
--- a/Nibriboard/Client/LineIncubator.cs
+++ b/Nibriboard/Client/LineIncubator.cs
@@ -21,7 +21,7 @@ namespace Nibriboard.Client
 		public int LinesCompleted { get; private set; } = 0;
 
 		/// <summary>
-		/// The number of liens that are still incubating and haven't been completed yet.
+		/// The number of lines that are still incubating and haven't been completed yet.
 		/// </summary>
 		public int IncompletedLines {
 			get {
diff --git a/Nibriboard/Client/NibriClient.cs b/Nibriboard/Client/NibriClient.cs
index 1fa1390..73d76fa 100644
--- a/Nibriboard/Client/NibriClient.cs
+++ b/Nibriboard/Client/NibriClient.cs
@@ -55,7 +55,8 @@ namespace Nibriboard.Client
 			["CursorPosition"] = typeof(CursorPositionMessage),
 			["PlaneChange"] = typeof(PlaneChangeMessage),
 			["ChunkUpdateRequest"] = typeof(ChunkUpdateRequestMessage),
-			["LinePartMessage"] = typeof(LinePartMessage)
+			["LinePartMessage"] = typeof(LinePartMessage),
+			["LineCompleteMessage"] = typeof(LineCompleteMessage)
 		};
 
 		/// <summary>
@@ -327,16 +328,37 @@ namespace Nibriboard.Client
 			return Task.CompletedTask;
 		}
 
+		/// <summary>
+		/// Handles messages containing a fragment of a line from the client.
+		/// </summary>
+		/// <param name="message">The message to process.</param>
 		protected Task handleLinePartMessage(LinePartMessage message)
 		{
 			// Forward the line part to everyone on this plane
 			manager.BroadcastPlane(this, message);
 
-			throw new NotImplementedException();
+			List<LocationReference> linePoints = new List<LocationReference>(message.Points.Count);
+			foreach(Vector2 point in message.Points)
+				linePoints.Add(new LocationReference(CurrentPlane, point.X, point.Y));
+			
+			manager.LineIncubator.AddBit(message.LineId, linePoints);
 
 			return Task.CompletedTask;
 		}
 
+		/// <summary>
+		/// Handles notifications from clients telling us that they've finished drawing a line.
+		/// </summary>
+		/// <param name="message">The message to handle.</param>
+		protected async Task handleLineCompleteMessage(LineCompleteMessage message)
+		{
+			DrawnLine line = manager.LineIncubator.CompleteLine(message.LineId);
+			line.LineWidth = message.LineWidth;
+			line.Colour = message.LineColour;
+
+			await CurrentPlane.AddLine(line);
+		}
+
 		#endregion
 
 
diff --git a/Nibriboard/Client/NibriClientManager.cs b/Nibriboard/Client/NibriClientManager.cs
index 2289986..bf866fc 100644
--- a/Nibriboard/Client/NibriClientManager.cs
+++ b/Nibriboard/Client/NibriClientManager.cs
@@ -22,6 +22,8 @@ namespace Nibriboard.Client
 		private ClientSettings clientSettings;
 		public List<NibriClient> Clients = new List<NibriClient>();
 
+		public LineIncubator LineIncubator = new LineIncubator();
+
 		/// <summary>
 		/// The cancellation token that's used by the main server to tell us when we should shut down.
 		/// </summary>
diff --git a/Nibriboard/RippleSpace/Chunk.cs b/Nibriboard/RippleSpace/Chunk.cs
index 0322a0f..8400577 100644
--- a/Nibriboard/RippleSpace/Chunk.cs
+++ b/Nibriboard/RippleSpace/Chunk.cs
@@ -7,6 +7,32 @@ using System.Runtime.Serialization;
 
 namespace Nibriboard.RippleSpace
 {
+	public enum ChunkUpdateType
+	{
+		/// <summary>
+		/// Something was added to the chunk.
+		/// </summary>
+		Addition,
+		/// <summary>
+		/// Something was deleted form the chunk.
+		/// </summary>
+		Deletion,
+		/// <summary>
+		/// A combination of additions and deletions were made to the chunk's contents.
+		/// </summary>
+		Combination
+	}
+
+	public class ChunkUpdateEventArgs : EventArgs
+	{
+		/// <summary>
+		/// The type of update made to the chunk
+		/// </summary>
+		public ChunkUpdateType UpdateType { get; set; }
+	}
+
+	public delegate void ChunkUpdateEvent(object sender, ChunkUpdateEventArgs eventArgs);
+
 	/// <summary>
 	/// Represents a single chunk of an infinite <see cref="NibriboardServer.RippleSpace.Plane" />.
 	/// </summary>
@@ -33,7 +59,10 @@ namespace Nibriboard.RippleSpace
 		/// </summary>
 		public readonly ChunkReference Location;
 
-
+		/// <summary>
+		/// Fired when this chunk is updated.
+		/// </summary>
+		public event ChunkUpdateEvent ChunkUpdateEvent;
 		/// <summary>
 		/// The time at which this chunk was loaded.
 		/// </summary>
@@ -83,9 +112,8 @@ namespace Nibriboard.RippleSpace
 		}
 
 		/// <summary>
-		/// Whether this chunk could, theorectically, be unloaded. Of course,
-		/// the server may decide it doesn't need to unload us even if we're
-		/// inactive.
+		/// Whether this chunk could, theorectically, be unloaded.
+		/// This method takes into account whether this is a primary chunk or not.
 		/// </summary>
 		public bool CouldUnload
 		{
@@ -148,6 +176,8 @@ namespace Nibriboard.RippleSpace
 				lines.Add(newLine);
 				i++;
 			}
+
+			ChunkUpdateEvent(this, new ChunkUpdateEventArgs() { UpdateType = ChunkUpdateType.Addition });
 		}
 
 		public IEnumerator<DrawnLine> GetEnumerator()
diff --git a/Nibriboard/RippleSpace/Plane.cs b/Nibriboard/RippleSpace/Plane.cs
index 5bfc7a0..ade1447 100644
--- a/Nibriboard/RippleSpace/Plane.cs
+++ b/Nibriboard/RippleSpace/Plane.cs
@@ -2,6 +2,8 @@
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Runtime.Serialization;
 
 namespace Nibriboard.RippleSpace
 {
@@ -37,16 +39,60 @@ namespace Nibriboard.RippleSpace
 		/// </summary>
 		public int PrimaryChunkAreaSize = 10;
 
+		/// <summary>
+		/// The minimum number of potentially unloadable chunks that we should have
+		/// before considering unloading some chunks
+		/// </summary>
+		public int MinUnloadeableChunks = 50;
+
+		/// <summary>
+		/// The soft limit on the number of chunks we can have loaded before we start
+		/// bothering to try and unload any chunks
+		/// </summary>
+		public int SoftLoadedChunkLimit;
+
+		/// <summary>
+		/// Fired when one of the chunks on this plane updates.
+		/// </summary>
+		public event ChunkUpdateEvent OnChunkUpdate;
+
 		/// <summary>
 		/// The chunkspace that holds the currently loaded and active chunks.
 		/// </summary>
 		protected Dictionary<ChunkReference, Chunk> loadedChunkspace = new Dictionary<ChunkReference, Chunk>();
 
+		/// <summary>
+		/// The number of chunks that this plane currently has laoded into active memory.
+		/// </summary>
+		public int LoadedChunks {
+			get {
+				return loadedChunkspace.Count;
+			}
+		}
+		/// <summary>
+		/// The number of potentially unloadable chunks this plane currently has.
+		/// </summary>
+		public int UnloadableChunks {
+			get {
+				int result = 0;
+				foreach(KeyValuePair<ChunkReference, Chunk> chunkEntry in loadedChunkspace) {
+					if(chunkEntry.Value.CouldUnload)
+						result++;
+				}
+				return result;
+			}
+		}
+
 		public Plane(string inName, int inChunkSize)
 		{
 			Name = inName;
 			ChunkSize = inChunkSize;
 
+			// Set the soft loaded chunk limit to double the number of chunks in the
+			// primary chunks area
+			// Note that the primary chunk area is a radius around (0, 0) - not the diameter
+			SoftLoadedChunkLimit = PrimaryChunkAreaSize * PrimaryChunkAreaSize * 4;
+
 			StorageDirectory = $"./Planes/{Name}";
 		}
 		/// <summary>
@@ -74,6 +120,7 @@ namespace Nibriboard.RippleSpace
 			// return it fast.
 			string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename());
 			Chunk loadedChunk = await Chunk.FromFile(this, chunkFilePath);
+			loadedChunk.OnChunkUpdate += HandleChunkUpdate;
 			loadedChunkspace.Add(chunkLocation, loadedChunk);
 
 			return loadedChunk;
@@ -96,9 +143,43 @@ namespace Nibriboard.RippleSpace
 			}
 		}
 
-		public async Task PerformMaintenance()
+		public void PerformMaintenance()
 		{
-			// TODO: Perform maintenance here
+			// Be lazy and don't bother to perform maintenance if it's not needed
+			if(LoadedChunks < SoftLoadedChunkLimit ||
+			   UnloadableChunks < MinUnloadeableChunks)
+				return;
+			
+			foreach(KeyValuePair<ChunkReference, Chunk> chunkEntry in loadedChunkspace)
+			{
+				if(!chunkEntry.Value.CouldUnload)
+					continue;
+
+				// This chunk has been inactive for a while - let's serialise it and save it to disk
+				Stream chunkSerializationSink = new FileStream(
+					Path.Combine(StorageDirectory, chunkEntry.Key.AsFilename()),
+					FileMode.Create,
+					FileAccess.Write,
+					FileShare.None
+				);
+				IFormatter binaryFormatter = new BinaryFormatter();
+				binaryFormatter.Serialize(chunkSerializationSink, chunkEntry.Value);
+
+				// Remove the chunk from the loaded chunkspace
+				loadedChunkspace.Remove(chunkEntry.Key);
+			}
+		}
+
+		/// <summary>
+		/// Handles chunk updates from the individual loaded chunks on this plane.
+		/// Re-emits chunk updates it catches wind of at plane-level.
+		/// </summary>
+		/// <param name="sender">The chunk responsible for the update.</param>
+		/// <param name="eventArgs">The event arguments associated with the chunk update.</param>
+		protected void HandleChunkUpdate(object sender, ChunkUpdateEventArgs eventArgs)
+		{
+			// Make the chunk update bubble up to plane-level
+			OnChunkUpdate(sender, eventArgs);
 		}
 	}
 }
diff --git a/Nibriboard/RippleSpace/RippleSpaceManager.cs b/Nibriboard/RippleSpace/RippleSpaceManager.cs
index a0fd84b..fd87ec6 100644
--- a/Nibriboard/RippleSpace/RippleSpaceManager.cs
+++ b/Nibriboard/RippleSpace/RippleSpaceManager.cs
@@ -62,7 +62,7 @@ namespace Nibriboard.RippleSpace
 				Stopwatch maintenanceStopwatch = Stopwatch.StartNew();
 
 				foreach (Plane plane in Planes)
-					await plane.PerformMaintenance();
+					plane.PerformMaintenance();
 
 				LastMaintenanceDuration = maintenanceStopwatch.ElapsedMilliseconds;
 
