Merge changes If50dcc0e,I17d591a5,I9c1cabda,Ib73d395b,I7e923b0d, ...

* changes:
  Only schedule OnRun callbacks right after construction in aos
  Split function scheduler out
  Fix transmit timestamp logic in SimulatedNetworkBridge
  Unit test for mismatched configs for server/client
  Convert unreachable simulated network bridge code to CHECKs.
  aos: Fix `--force_timestamp_loading` for single-node logs
  Log, replay, and solve with transmit timestamps
  Add boot info to NoncausalTimestampFilter debugging
  Logger: Pipe the monotonic_remote_transmit_time through event loop
  Remove unused kLogMessageAndDeliveryTime
  Fix starter_test under msan
  memory_mapped_queue: Use system_page_size
  Logger: Define contract to record transmit time for reliable messages
  Add note to static fbs Vector reserve() method
  Add a test reproducing a log reading failure
  Fix move ResizeableObject move constructor
  Logger: Print solve number along with candidate solution for debug info
  Call set_name() on more AOS timers
  Fix crash in message_bridge_server
  Fix typo in message_bridge_auth_client_lib.cc
  Fix Python wheel generation
  Fix LogReader destruction problem
  Make doctests only compatible with x86_64
  Fix possible data race in `aos/network/multinode_timestamp_filter.h`
  Fix handling of empty C++ comments in JSON parsing
  rename variables in aos Logger
  Fix AOS logging when using non-EventLoop configuration
  Add SendJson to AOS Sender
  Add note on size units in file_operations.h
  Reduce discrepancies between FlatbufferToJson versions
  Reduce flakeness of shm_event_loop_test
  aos: Make SetCurrentThreadAffinity errors a bit nicer
diff --git a/aos/configuration.cc b/aos/configuration.cc
index 6cae0d4..4de5332 100644
--- a/aos/configuration.cc
+++ b/aos/configuration.cc
@@ -1729,5 +1729,39 @@
   return MergeWithConfig(config, new_channel_config);
 }
 
+FlatbufferDetachedBuffer<Configuration> GetPartialConfiguration(
+    const Configuration &configuration,
+    std::function<bool(const Channel &)> should_include_channel) {
+  // create new_configuration1, containing everything except the `channels`
+  // field.
+  FlatbufferDetachedBuffer<Configuration> new_configuration1 =
+      RecursiveCopyFlatBuffer(&configuration);
+  new_configuration1.mutable_message()->clear_channels();
+
+  // create new_configuration2, containing only the `channels` field.
+  flatbuffers::FlatBufferBuilder fbb;
+  std::vector<flatbuffers::Offset<Channel>> new_channels_vec;
+  for (const auto &channel : *configuration.channels()) {
+    CHECK_NOTNULL(channel);
+    if (should_include_channel(*channel)) {
+      new_channels_vec.push_back(RecursiveCopyFlatBuffer(channel, &fbb));
+    }
+  }
+  flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Channel>>>
+      new_channels_offset = fbb.CreateVector(new_channels_vec);
+  Configuration::Builder new_configuration2_builder(fbb);
+  new_configuration2_builder.add_channels(new_channels_offset);
+  fbb.Finish(new_configuration2_builder.Finish());
+  FlatbufferDetachedBuffer<Configuration> new_configuration2 = fbb.Release();
+
+  // Merge the configuration containing channels with the configuration
+  // containing everything else, creating a complete configuration.
+  const aos::FlatbufferDetachedBuffer<Configuration> raw_subset_configuration =
+      MergeFlatBuffers(&new_configuration1.message(),
+                       &new_configuration2.message());
+
+  // Use MergeConfiguration to clean up redundant schemas.
+  return configuration::MergeConfiguration(raw_subset_configuration);
+}
 }  // namespace configuration
 }  // namespace aos
diff --git a/aos/configuration.h b/aos/configuration.h
index 01b9e7c..7ef2e6e 100644
--- a/aos/configuration.h
+++ b/aos/configuration.h
@@ -248,6 +248,12 @@
     aos::FlatbufferVector<reflection::Schema> schema,
     const aos::Node *source_node = nullptr, ChannelT overrides = {});
 
+// Build a new configuration that only contains the channels we want to
+// include. This is useful for excluding obsolete or deprecated channels, so
+// they don't appear in the configuration when reading the log.
+FlatbufferDetachedBuffer<Configuration> GetPartialConfiguration(
+    const Configuration &configuration,
+    std::function<bool(const Channel &)> should_include_channel);
 }  // namespace configuration
 
 // Compare and equality operators for Channel.  Note: these only check the name
diff --git a/aos/configuration_test.cc b/aos/configuration_test.cc
index e5fe303..eb08d78 100644
--- a/aos/configuration_test.cc
+++ b/aos/configuration_test.cc
@@ -1166,4 +1166,68 @@
   ASSERT_EQ(971, channel->frequency());
 }
 
+// Create a new configuration with the specified channel removed.
+// Initially there must be exactly one channel in the base_config that matches
+// the criteria. Check to make sure the new configuration has one less channel,
+// and that channel is the specified channel.
+void TestGetPartialConfiguration(const Configuration &base_config,
+                                 std::string_view test_channel_name,
+                                 std::string_view test_channel_type) {
+  const Channel *channel_from_base_config = GetChannel(
+      &base_config, test_channel_name, test_channel_type, "", nullptr);
+  ASSERT_TRUE(channel_from_base_config != nullptr);
+
+  const FlatbufferDetachedBuffer<Configuration> new_config =
+      configuration::GetPartialConfiguration(
+          base_config,
+          // should_include_channel function
+          [test_channel_name, test_channel_type](const Channel &channel) {
+            if (channel.name()->string_view() == test_channel_name &&
+                channel.type()->string_view() == test_channel_type) {
+              LOG(INFO) << "Omitting channel from save_log, channel: "
+                        << channel.name()->string_view() << ", "
+                        << channel.type()->string_view();
+              return false;
+            }
+            return true;
+          });
+
+  EXPECT_EQ(new_config.message().channels()->size(),
+            base_config.channels()->size() - 1);
+
+  channel_from_base_config = GetChannel(&base_config, test_channel_name,
+                                        test_channel_type, "", nullptr);
+  EXPECT_TRUE(channel_from_base_config != nullptr);
+
+  const Channel *channel_from_new_config =
+      GetChannel(new_config, test_channel_name, test_channel_type, "", nullptr);
+  EXPECT_TRUE(channel_from_new_config == nullptr);
+}
+
+// Tests that we can use a utility to remove individual channels from a
+// single-node config.
+TEST_F(ConfigurationTest, RemoveChannelsFromConfigSingleNode) {
+  FlatbufferDetachedBuffer<Configuration> base_config =
+      ReadConfig(ArtifactPath("aos/testdata/config1.json"));
+
+  constexpr std::string_view test_channel_name = "/foo2";
+  constexpr std::string_view test_channel_type = ".aos.bar";
+
+  TestGetPartialConfiguration(base_config.message(), test_channel_name,
+                              test_channel_type);
+}
+
+// Tests that we can use a utility to remove individual channels from a
+// multi-node config.
+TEST_F(ConfigurationTest, RemoveChannelsFromConfigMultiNode) {
+  FlatbufferDetachedBuffer<Configuration> base_config =
+      ReadConfig(ArtifactPath("aos/testdata/good_multinode.json"));
+
+  constexpr std::string_view test_channel_name = "/batman";
+  constexpr std::string_view test_channel_type = ".aos.baz";
+
+  TestGetPartialConfiguration(base_config.message(), test_channel_name,
+                              test_channel_type);
+}
+
 }  // namespace aos::configuration::testing
diff --git a/aos/events/BUILD b/aos/events/BUILD
index e500417..7f9c008 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -509,10 +509,11 @@
         ":multinode_pingpong_test_combined_config",
         ":multinode_pingpong_test_split_config",
     ],
-    shard_count = 4,
+    shard_count = 16,
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
         ":event_loop_param_test",
+        ":function_scheduler",
         ":message_counter",
         ":ping_lib",
         ":pong_lib",
@@ -744,6 +745,17 @@
     ],
 )
 
+cc_library(
+    name = "function_scheduler",
+    srcs = [
+        "function_scheduler.cc",
+    ],
+    hdrs = [
+        "function_scheduler.h",
+    ],
+    deps = [":event_loop"],
+)
+
 cc_binary(
     name = "aos_timing_report_streamer",
     srcs = ["aos_timing_report_streamer.cc"],
diff --git a/aos/events/context.h b/aos/events/context.h
index 6648949..c50b5c0 100644
--- a/aos/events/context.h
+++ b/aos/events/context.h
@@ -24,6 +24,9 @@
   monotonic_clock::time_point monotonic_remote_time;
   realtime_clock::time_point realtime_remote_time;
 
+  // Time that the message was published over the network on the remote node.
+  monotonic_clock::time_point monotonic_remote_transmit_time;
+
   // Index in the queue.
   uint32_t queue_index;
   // Index into the remote queue.  Useful to determine if data was lost.  In a
diff --git a/aos/events/event_loop.cc b/aos/events/event_loop.cc
index ba71b97..424971d 100644
--- a/aos/events/event_loop.cc
+++ b/aos/events/event_loop.cc
@@ -83,9 +83,11 @@
 RawSender::Error RawSender::DoSend(
     const SharedSpan data, monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
+    monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid) {
   return DoSend(data->data(), data->size(), monotonic_remote_time,
-                realtime_remote_time, remote_queue_index, source_boot_uuid);
+                realtime_remote_time, monotonic_remote_transmit_time,
+                remote_queue_index, source_boot_uuid);
 }
 
 void RawSender::RecordSendResult(const Error error, size_t message_size) {
@@ -113,6 +115,7 @@
       timing_(event_loop_->ChannelIndex(channel)) {
   context_.monotonic_event_time = monotonic_clock::min_time;
   context_.monotonic_remote_time = monotonic_clock::min_time;
+  context_.monotonic_remote_transmit_time = monotonic_clock::min_time;
   context_.realtime_event_time = realtime_clock::min_time;
   context_.realtime_remote_time = realtime_clock::min_time;
   context_.queue_index = 0xffffffff;
@@ -628,6 +631,7 @@
 void EventLoop::ClearContext() {
   context_.monotonic_event_time = monotonic_clock::min_time;
   context_.monotonic_remote_time = monotonic_clock::min_time;
+  context_.monotonic_remote_transmit_time = monotonic_clock::min_time;
   context_.realtime_event_time = realtime_clock::min_time;
   context_.realtime_remote_time = realtime_clock::min_time;
   context_.queue_index = 0xffffffffu;
@@ -642,6 +646,7 @@
     monotonic_clock::time_point monotonic_event_time) {
   context_.monotonic_event_time = monotonic_event_time;
   context_.monotonic_remote_time = monotonic_clock::min_time;
+  context_.monotonic_remote_transmit_time = monotonic_clock::min_time;
   context_.realtime_event_time = realtime_clock::min_time;
   context_.realtime_remote_time = realtime_clock::min_time;
   context_.queue_index = 0xffffffffu;
diff --git a/aos/events/event_loop.h b/aos/events/event_loop.h
index 1ff0fb7..83393c2 100644
--- a/aos/events/event_loop.h
+++ b/aos/events/event_loop.h
@@ -140,12 +140,14 @@
   // size() bytes into the data backed by data().  They then call Send to send.
   // Returns Error::kOk on a successful send, or
   // Error::kMessagesSentTooFast if messages were sent too fast. If provided,
-  // monotonic_remote_time, realtime_remote_time, and remote_queue_index are
-  // attached to the message and are available in the context on the read side.
-  // If they are not populated, the read side will get the sent times instead.
+  // monotonic_remote_time, realtime_remote_time,
+  // monotonic_remote_transmit_time, and remote_queue_index are attached to the
+  // message and are available in the context on the read side. If they are not
+  // populated, the read side will get the sent times instead.
   Error Send(size_t size);
   Error Send(size_t size, monotonic_clock::time_point monotonic_remote_time,
              realtime_clock::time_point realtime_remote_time,
+             monotonic_clock::time_point monotonic_remote_transmit_time,
              uint32_t remote_queue_index, const UUID &source_boot_uuid);
 
   // Sends a single block of data by copying it.
@@ -155,6 +157,7 @@
   Error Send(const void *data, size_t size,
              monotonic_clock::time_point monotonic_remote_time,
              realtime_clock::time_point realtime_remote_time,
+             monotonic_clock::time_point monotonic_remote_transmit_time,
              uint32_t remote_queue_index, const UUID &source_boot_uuid);
 
   // CHECKs that no sending Error occurred and logs the channel_ data if
@@ -168,6 +171,7 @@
   Error Send(const SharedSpan data,
              monotonic_clock::time_point monotonic_remote_time,
              realtime_clock::time_point realtime_remote_time,
+             monotonic_clock::time_point monotonic_remote_transmit_time,
              uint32_t remote_queue_index, const UUID &remote_boot_uuid);
   const Channel *channel() const { return channel_; }
 
@@ -216,21 +220,22 @@
  private:
   friend class EventLoop;
 
-  virtual Error DoSend(const void *data, size_t size,
-                       monotonic_clock::time_point monotonic_remote_time,
-                       realtime_clock::time_point realtime_remote_time,
-                       uint32_t remote_queue_index,
-                       const UUID &source_boot_uuid) = 0;
-  virtual Error DoSend(size_t size,
-                       monotonic_clock::time_point monotonic_remote_time,
-                       realtime_clock::time_point realtime_remote_time,
-                       uint32_t remote_queue_index,
-                       const UUID &source_boot_uuid) = 0;
-  virtual Error DoSend(const SharedSpan data,
-                       monotonic_clock::time_point monotonic_remote_time,
-                       realtime_clock::time_point realtime_remote_time,
-                       uint32_t remote_queue_index,
-                       const UUID &source_boot_uuid);
+  virtual Error DoSend(
+      const void *data, size_t size,
+      monotonic_clock::time_point monotonic_remote_time,
+      realtime_clock::time_point realtime_remote_time,
+      monotonic_clock::time_point monotonic_remote_transmit_time,
+      uint32_t remote_queue_index, const UUID &source_boot_uuid) = 0;
+  virtual Error DoSend(
+      size_t size, monotonic_clock::time_point monotonic_remote_time,
+      realtime_clock::time_point realtime_remote_time,
+      monotonic_clock::time_point monotonic_remote_transmit_time,
+      uint32_t remote_queue_index, const UUID &source_boot_uuid) = 0;
+  virtual Error DoSend(
+      const SharedSpan data, monotonic_clock::time_point monotonic_remote_time,
+      realtime_clock::time_point realtime_remote_time,
+      monotonic_clock::time_point monotonic_remote_transmit_time,
+      uint32_t remote_queue_index, const UUID &source_boot_uuid);
 
   void RecordSendResult(const Error error, size_t message_size);
 
@@ -506,6 +511,16 @@
   // the buffer the caller can fill out.
   int buffer_index() const { return CHECK_NOTNULL(sender_)->buffer_index(); }
 
+  // Convenience function to build and send a message created from JSON
+  // representation.
+  RawSender::Error SendJson(std::string_view json) {
+    auto builder = MakeBuilder();
+    flatbuffers::Offset<T> json_offset =
+        aos::JsonToFlatbuffer<T>(json, builder.fbb());
+    CHECK(!json_offset.IsNull()) << ": Invalid JSON";
+    return builder.Send(json_offset);
+  }
+
  private:
   friend class EventLoop;
   Sender(std::unique_ptr<RawSender> sender) : sender_(std::move(sender)) {}
diff --git a/aos/events/event_loop_param_test.cc b/aos/events/event_loop_param_test.cc
index 58bfd9b..4885f5c 100644
--- a/aos/events/event_loop_param_test.cc
+++ b/aos/events/event_loop_param_test.cc
@@ -133,6 +133,33 @@
   EXPECT_TRUE(happened);
 }
 
+// Tests that a static sender's Builder object can be moved safely.
+TEST_P(AbstractEventLoopTest, StaticBuilderMoveConstructor) {
+  auto loop1 = MakePrimary();
+
+  aos::Sender<TestMessageStatic> sender =
+      loop1->MakeSender<TestMessageStatic>("/test");
+  aos::Fetcher<TestMessage> fetcher = loop1->MakeFetcher<TestMessage>("/test");
+  std::optional<aos::Sender<TestMessageStatic>::StaticBuilder> moved_to_builder;
+  {
+    aos::Sender<TestMessageStatic>::StaticBuilder moved_from_builder =
+        sender.MakeStaticBuilder();
+    moved_to_builder.emplace(std::move(moved_from_builder));
+  }
+
+  loop1->OnRun([this, &moved_to_builder]() {
+    moved_to_builder.value()->set_value(200);
+    CHECK(moved_to_builder.value().builder()->Verify());
+    moved_to_builder.value().CheckOk(moved_to_builder.value().Send());
+    this->Exit();
+  });
+
+  ASSERT_FALSE(fetcher.Fetch());
+  Run();
+  ASSERT_TRUE(fetcher.Fetch());
+  EXPECT_EQ(200, fetcher->value());
+}
+
 // Tests that watcher can receive messages from a sender, sent via SendDetached.
 TEST_P(AbstractEventLoopTest, BasicSendDetached) {
   auto loop1 = Make();
@@ -162,6 +189,29 @@
   EXPECT_EQ(fetcher->value(), 200);
 }
 
+// Tests that fetcher can receive messages from a sender, sent via SendJson.
+TEST_P(AbstractEventLoopTest, BasicSendJson) {
+  auto loop1 = Make();
+  auto loop2 = MakePrimary();
+
+  aos::Sender<TestMessage> sender = loop1->MakeSender<TestMessage>("/test");
+  sender.CheckOk(sender.SendJson(R"json({"value":201})json"));
+
+  auto fetcher = loop2->MakeFetcher<TestMessage>("/test");
+  ASSERT_TRUE(fetcher.Fetch());
+  EXPECT_EQ(fetcher->value(), 201);
+}
+
+// Tests that invalid JSON isn't sent.
+TEST_P(AbstractEventLoopDeathTest, InvalidSendJson) {
+  auto loop1 = Make();
+  auto loop2 = MakePrimary();
+
+  aos::Sender<TestMessage> sender = loop1->MakeSender<TestMessage>("/test");
+  EXPECT_DEATH({ sender.CheckOk(sender.SendJson(R"json({"val)json")); },
+               "Invalid JSON");
+}
+
 // Verifies that a no-arg watcher will not have a data pointer.
 TEST_P(AbstractEventLoopTest, NoArgNoData) {
   auto loop1 = Make();
@@ -308,6 +358,8 @@
 
   EXPECT_EQ(fetcher.context().monotonic_event_time, monotonic_clock::min_time);
   EXPECT_EQ(fetcher.context().monotonic_remote_time, monotonic_clock::min_time);
+  EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
   EXPECT_EQ(fetcher.context().realtime_event_time, realtime_clock::min_time);
   EXPECT_EQ(fetcher.context().realtime_remote_time, realtime_clock::min_time);
   EXPECT_EQ(fetcher.context().source_boot_uuid, UUID::Zero());
@@ -331,6 +383,8 @@
   const aos::realtime_clock::time_point realtime_now = loop2->realtime_now();
   EXPECT_EQ(fetcher.context().monotonic_event_time,
             fetcher.context().monotonic_remote_time);
+  EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
   EXPECT_EQ(fetcher.context().realtime_event_time,
             fetcher.context().realtime_remote_time);
 
@@ -379,6 +433,8 @@
               monotonic_clock::min_time);
     EXPECT_EQ(fetcher.context().monotonic_remote_time,
               monotonic_clock::min_time);
+    EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+              monotonic_clock::min_time);
     EXPECT_EQ(fetcher.context().realtime_event_time, realtime_clock::min_time);
     EXPECT_EQ(fetcher.context().realtime_remote_time, realtime_clock::min_time);
     EXPECT_EQ(fetcher.context().source_boot_uuid, UUID::Zero());
@@ -399,6 +455,8 @@
 
   EXPECT_EQ(fetcher.context().monotonic_event_time, monotonic_clock::min_time);
   EXPECT_EQ(fetcher.context().monotonic_remote_time, monotonic_clock::min_time);
+  EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
   EXPECT_EQ(fetcher.context().realtime_event_time, realtime_clock::min_time);
   EXPECT_EQ(fetcher.context().realtime_remote_time, realtime_clock::min_time);
   EXPECT_EQ(fetcher.context().source_boot_uuid, UUID::Zero());
@@ -420,6 +478,8 @@
             fetcher.context().monotonic_remote_time);
   EXPECT_EQ(fetcher.context().realtime_event_time,
             fetcher.context().realtime_remote_time);
+  EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
 
   EXPECT_GE(fetcher.context().monotonic_event_time, monotonic_now - kEpsilon);
   EXPECT_LE(fetcher.context().monotonic_event_time, monotonic_now + kEpsilon);
@@ -1414,6 +1474,8 @@
   auto test_timer = loop->AddTimer([this, &times, &expected_times, &loop]() {
     times.push_back(loop->monotonic_now());
     EXPECT_EQ(loop->context().monotonic_remote_time, monotonic_clock::min_time);
+    EXPECT_EQ(loop->context().monotonic_remote_transmit_time,
+              monotonic_clock::min_time);
     EXPECT_EQ(loop->context().realtime_event_time, realtime_clock::min_time);
     EXPECT_EQ(loop->context().realtime_remote_time, realtime_clock::min_time);
     EXPECT_EQ(loop->context().source_boot_uuid, loop->boot_uuid());
@@ -1864,6 +1926,8 @@
     EXPECT_EQ(loop1->context().realtime_remote_time,
               loop1->context().realtime_event_time);
     EXPECT_EQ(loop1->context().source_boot_uuid, loop1->boot_uuid());
+    EXPECT_EQ(loop1->context().monotonic_remote_transmit_time,
+              monotonic_clock::min_time);
 
     const aos::monotonic_clock::time_point monotonic_now =
         loop1->monotonic_now();
@@ -1911,6 +1975,8 @@
             fetcher.context().realtime_remote_time);
   EXPECT_EQ(fetcher.context().monotonic_event_time,
             fetcher.context().monotonic_remote_time);
+  EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
   EXPECT_EQ(fetcher.context().source_boot_uuid, loop1->boot_uuid());
 
   EXPECT_TRUE(monotonic_time_offset > ::std::chrono::milliseconds(-500))
@@ -1962,6 +2028,8 @@
 
     EXPECT_EQ(loop1->context().monotonic_remote_time,
               loop1->context().monotonic_event_time);
+    EXPECT_EQ(loop1->context().monotonic_remote_transmit_time,
+              monotonic_clock::min_time);
     EXPECT_EQ(loop1->context().realtime_remote_time,
               loop1->context().realtime_event_time);
     EXPECT_EQ(loop1->context().source_boot_uuid, loop1->boot_uuid());
@@ -1998,6 +2066,8 @@
             fetcher.context().realtime_remote_time);
   EXPECT_EQ(fetcher.context().monotonic_event_time,
             fetcher.context().monotonic_remote_time);
+  EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
   EXPECT_EQ(fetcher.context().source_boot_uuid, loop1->boot_uuid());
 
   EXPECT_TRUE(monotonic_time_offset > ::std::chrono::milliseconds(-500))
@@ -2056,6 +2126,8 @@
 
             EXPECT_EQ(loop1->context().monotonic_remote_time,
                       monotonic_clock::min_time);
+            EXPECT_EQ(loop1->context().monotonic_remote_transmit_time,
+                      monotonic_clock::min_time);
             EXPECT_EQ(loop1->context().source_boot_uuid, loop1->boot_uuid());
             EXPECT_EQ(loop1->context().realtime_event_time,
                       realtime_clock::min_time);
@@ -3066,6 +3138,8 @@
       aos::monotonic_clock::time_point(chrono::seconds(1501));
   const aos::realtime_clock::time_point realtime_remote_time =
       aos::realtime_clock::time_point(chrono::seconds(3132));
+  const aos::monotonic_clock::time_point monotonic_remote_transmit_time =
+      aos::monotonic_clock::time_point(chrono::seconds(1601));
   const uint32_t remote_queue_index = 0x254971;
   const UUID source_boot_uuid = UUID::Random();
 
@@ -3080,7 +3154,8 @@
   loop2->OnRun([&]() {
     EXPECT_EQ(sender->Send(kMessage.span().data(), kMessage.span().size(),
                            monotonic_remote_time, realtime_remote_time,
-                           remote_queue_index, source_boot_uuid),
+                           monotonic_remote_transmit_time, remote_queue_index,
+                           source_boot_uuid),
               RawSender::Error::kOk);
   });
 
@@ -3088,9 +3163,9 @@
   loop2->MakeRawWatcher(
       configuration::GetChannel(loop2->configuration(), "/test",
                                 "aos.TestMessage", "", nullptr),
-      [this, monotonic_remote_time, realtime_remote_time, source_boot_uuid,
-       remote_queue_index, &fetcher,
-       &happened](const Context &context, const void * /*message*/) {
+      [this, monotonic_remote_time, realtime_remote_time,
+       monotonic_remote_transmit_time, source_boot_uuid, remote_queue_index,
+       &fetcher, &happened](const Context &context, const void * /*message*/) {
         happened = true;
         EXPECT_EQ(monotonic_remote_time, context.monotonic_remote_time);
         EXPECT_EQ(realtime_remote_time, context.realtime_remote_time);
@@ -3102,6 +3177,8 @@
                   fetcher->context().monotonic_remote_time);
         EXPECT_EQ(realtime_remote_time,
                   fetcher->context().realtime_remote_time);
+        EXPECT_EQ(monotonic_remote_transmit_time,
+                  fetcher->context().monotonic_remote_transmit_time);
 
         this->Exit();
       });
@@ -3109,6 +3186,19 @@
   EXPECT_FALSE(happened);
   Run();
   EXPECT_TRUE(happened);
+
+  // Confirm everything goes back.
+  EXPECT_EQ(loop2->context().monotonic_event_time, monotonic_clock::min_time);
+  EXPECT_EQ(loop2->context().monotonic_remote_time, monotonic_clock::min_time);
+  EXPECT_EQ(loop2->context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
+  EXPECT_EQ(loop2->context().realtime_event_time, realtime_clock::min_time);
+  EXPECT_EQ(loop2->context().realtime_remote_time, realtime_clock::min_time);
+  EXPECT_EQ(loop2->context().source_boot_uuid, loop2->boot_uuid());
+  EXPECT_EQ(loop2->context().queue_index, 0xffffffffu);
+  EXPECT_EQ(loop2->context().size, 0u);
+  EXPECT_EQ(loop2->context().data, nullptr);
+  EXPECT_EQ(loop2->context().buffer_index, -1);
 }
 
 // Tests that a raw sender fills out sent data.
@@ -3356,6 +3446,8 @@
 
   EXPECT_EQ(loop->context().monotonic_event_time, monotonic_clock::min_time);
   EXPECT_EQ(loop->context().monotonic_remote_time, monotonic_clock::min_time);
+  EXPECT_EQ(loop->context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
   EXPECT_EQ(loop->context().realtime_event_time, realtime_clock::min_time);
   EXPECT_EQ(loop->context().realtime_remote_time, realtime_clock::min_time);
   EXPECT_EQ(loop->context().source_boot_uuid, loop->boot_uuid());
@@ -3373,6 +3465,8 @@
     monotonic_event_time_on_run = loop->context().monotonic_event_time;
     EXPECT_LE(monotonic_event_time_on_run, loop->monotonic_now());
     EXPECT_EQ(loop->context().monotonic_remote_time, monotonic_clock::min_time);
+    EXPECT_EQ(loop->context().monotonic_remote_transmit_time,
+              monotonic_clock::min_time);
     EXPECT_EQ(loop->context().realtime_event_time, realtime_clock::min_time);
     EXPECT_EQ(loop->context().realtime_remote_time, realtime_clock::min_time);
     EXPECT_EQ(loop->context().source_boot_uuid, loop->boot_uuid());
@@ -3392,6 +3486,8 @@
 
   EXPECT_EQ(loop->context().monotonic_event_time, monotonic_clock::min_time);
   EXPECT_EQ(loop->context().monotonic_remote_time, monotonic_clock::min_time);
+  EXPECT_EQ(loop->context().monotonic_remote_transmit_time,
+            monotonic_clock::min_time);
   EXPECT_EQ(loop->context().realtime_event_time, realtime_clock::min_time);
   EXPECT_EQ(loop->context().realtime_remote_time, realtime_clock::min_time);
   EXPECT_EQ(loop->context().source_boot_uuid, loop->boot_uuid());
@@ -3553,6 +3649,7 @@
 // RawSender::Error::kMessagesSentTooFast.
 TEST_P(AbstractEventLoopTest, SendingMessagesTooFast) {
   auto event_loop = MakePrimary();
+  event_loop->SetRuntimeRealtimePriority(5);
 
   auto sender = event_loop->MakeSender<TestMessage>("/test");
 
@@ -3567,21 +3664,29 @@
   int msgs_sent = 1;
   const int queue_size = TestChannelQueueSize(event_loop.get());
 
+  const int messages_per_ms = 2;
+  const auto kRepeatOffset = std::chrono::milliseconds(10);
+  const auto base_offset =
+      configuration::ChannelStorageDuration(event_loop->configuration(),
+                                            sender.channel()) -
+      (std::chrono::milliseconds(1) * (queue_size / 2) / messages_per_ms);
+
   const auto timer = event_loop->AddTimer([&]() {
-    const bool done = (msgs_sent == queue_size + 1);
-    ASSERT_EQ(
-        SendTestMessage(sender),
-        done ? RawSender::Error::kMessagesSentTooFast : RawSender::Error::kOk);
-    msgs_sent++;
-    if (done) {
-      Exit();
+    // Send in bursts to reduce scheduler load to make the test more
+    // reproducible.
+    for (int i = 0; i < messages_per_ms * kRepeatOffset.count(); ++i) {
+      const bool done = (msgs_sent == queue_size + 1);
+      ASSERT_EQ(SendTestMessage(sender),
+                done ? RawSender::Error::kMessagesSentTooFast
+                     : RawSender::Error::kOk);
+      msgs_sent++;
+      if (done) {
+        Exit();
+        return;
+      }
     }
   });
 
-  const auto kRepeatOffset = std::chrono::milliseconds(1);
-  const auto base_offset = configuration::ChannelStorageDuration(
-                               event_loop->configuration(), sender.channel()) -
-                           (kRepeatOffset * (queue_size / 2));
   event_loop->OnRun([&event_loop, &timer, &base_offset, &kRepeatOffset]() {
     timer->Schedule(event_loop->monotonic_now() + base_offset, kRepeatOffset);
   });
@@ -3595,6 +3700,7 @@
 // situation
 TEST_P(AbstractEventLoopTest, SendingAfterSendingTooFast) {
   auto event_loop = MakePrimary();
+  event_loop->SetRuntimeRealtimePriority(5);
 
   auto sender = event_loop->MakeSender<TestMessage>("/test");
 
diff --git a/aos/events/event_loop_runtime.h b/aos/events/event_loop_runtime.h
index 0505852..afc96e2 100644
--- a/aos/events/event_loop_runtime.h
+++ b/aos/events/event_loop_runtime.h
@@ -25,6 +25,7 @@
 
   int64_t monotonic_remote_time;
   int64_t realtime_remote_time;
+  int64_t monotonic_remote_transmit_time;
 
   uint32_t queue_index;
   uint32_t remote_queue_index;
@@ -48,6 +49,8 @@
               offsetof(RustContext, monotonic_remote_time));
 static_assert(offsetof(Context, realtime_remote_time) ==
               offsetof(RustContext, realtime_remote_time));
+static_assert(offsetof(Context, monotonic_remote_transmit_time) ==
+              offsetof(RustContext, monotonic_remote_transmit_time));
 static_assert(offsetof(Context, queue_index) ==
               offsetof(RustContext, queue_index));
 static_assert(offsetof(Context, remote_queue_index) ==
diff --git a/aos/events/event_loop_tmpl.h b/aos/events/event_loop_tmpl.h
index cc8e5c3..3f41ec1 100644
--- a/aos/events/event_loop_tmpl.h
+++ b/aos/events/event_loop_tmpl.h
@@ -197,15 +197,17 @@
 
 inline RawSender::Error RawSender::Send(size_t size) {
   return Send(size, monotonic_clock::min_time, realtime_clock::min_time,
-              0xffffffffu, event_loop_->boot_uuid());
+              monotonic_clock::min_time, 0xffffffffu, event_loop_->boot_uuid());
 }
 
 inline RawSender::Error RawSender::Send(
     size_t size, aos::monotonic_clock::time_point monotonic_remote_time,
     aos::realtime_clock::time_point realtime_remote_time,
+    aos::monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &uuid) {
-  const auto err = DoSend(size, monotonic_remote_time, realtime_remote_time,
-                          remote_queue_index, uuid);
+  const auto err =
+      DoSend(size, monotonic_remote_time, realtime_remote_time,
+             monotonic_remote_transmit_time, remote_queue_index, uuid);
   RecordSendResult(err, size);
   if (err == Error::kOk) {
     ftrace_.FormatMessage(
@@ -219,16 +221,18 @@
 
 inline RawSender::Error RawSender::Send(const void *data, size_t size) {
   return Send(data, size, monotonic_clock::min_time, realtime_clock::min_time,
-              0xffffffffu, event_loop_->boot_uuid());
+              monotonic_clock::min_time, 0xffffffffu, event_loop_->boot_uuid());
 }
 
 inline RawSender::Error RawSender::Send(
     const void *data, size_t size,
     aos::monotonic_clock::time_point monotonic_remote_time,
     aos::realtime_clock::time_point realtime_remote_time,
+    aos::monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &uuid) {
-  const auto err = DoSend(data, size, monotonic_remote_time,
-                          realtime_remote_time, remote_queue_index, uuid);
+  const auto err =
+      DoSend(data, size, monotonic_remote_time, realtime_remote_time,
+             monotonic_remote_transmit_time, remote_queue_index, uuid);
   RecordSendResult(err, size);
   if (err == RawSender::Error::kOk) {
     ftrace_.FormatMessage(
@@ -242,17 +246,20 @@
 
 inline RawSender::Error RawSender::Send(const SharedSpan data) {
   return Send(std::move(data), monotonic_clock::min_time,
-              realtime_clock::min_time, 0xffffffffu, event_loop_->boot_uuid());
+              realtime_clock::min_time, monotonic_clock::min_time, 0xffffffffu,
+              event_loop_->boot_uuid());
 }
 
 inline RawSender::Error RawSender::Send(
     const SharedSpan data,
     aos::monotonic_clock::time_point monotonic_remote_time,
     aos::realtime_clock::time_point realtime_remote_time,
+    aos::monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &uuid) {
   const size_t size = data->size();
-  const auto err = DoSend(std::move(data), monotonic_remote_time,
-                          realtime_remote_time, remote_queue_index, uuid);
+  const auto err =
+      DoSend(std::move(data), monotonic_remote_time, realtime_remote_time,
+             monotonic_remote_transmit_time, remote_queue_index, uuid);
   RecordSendResult(err, size);
   if (err == Error::kOk) {
     ftrace_.FormatMessage(
diff --git a/aos/events/event_scheduler.cc b/aos/events/event_scheduler.cc
index ae50917..df6ab81 100644
--- a/aos/events/event_scheduler.cc
+++ b/aos/events/event_scheduler.cc
@@ -68,6 +68,7 @@
 
 void EventScheduler::Startup() {
   ++boot_count_;
+  cached_event_list_monotonic_time_ = kInvalidCachedTime();
   CHECK(!is_running_);
   MaybeRunOnStartup();
   CHECK(called_started_);
diff --git a/aos/events/event_scheduler.h b/aos/events/event_scheduler.h
index 55fa9a4..84201ed 100644
--- a/aos/events/event_scheduler.h
+++ b/aos/events/event_scheduler.h
@@ -210,6 +210,10 @@
   void MaybeRunOnStartup();
   void MaybeRunOnRun();
 
+  constexpr monotonic_clock::time_point kInvalidCachedTime() {
+    return monotonic_clock::max_time;
+  }
+
   // Current execution time.
   monotonic_clock::time_point monotonic_now_ = monotonic_clock::epoch();
 
@@ -237,7 +241,7 @@
   bool called_started_ = false;
   std::optional<distributed_clock::time_point> cached_epoch_;
   monotonic_clock::time_point cached_event_list_monotonic_time_ =
-      monotonic_clock::max_time;
+      kInvalidCachedTime();
   distributed_clock::time_point cached_event_list_time_ =
       distributed_clock::max_time;
 
diff --git a/aos/events/function_scheduler.cc b/aos/events/function_scheduler.cc
new file mode 100644
index 0000000..94cb90d
--- /dev/null
+++ b/aos/events/function_scheduler.cc
@@ -0,0 +1,34 @@
+#include "aos/events/function_scheduler.h"
+
+namespace aos {
+
+FunctionScheduler::FunctionScheduler(aos::EventLoop *event_loop)
+    : event_loop_(event_loop), timer_(event_loop_->AddTimer([this]() {
+        RunFunctions(event_loop_->context().monotonic_event_time);
+      })) {
+  timer_->set_name("function_timer");
+  event_loop_->OnRun(
+      [this]() { RunFunctions(event_loop_->context().monotonic_event_time); });
+}
+
+void FunctionScheduler::ScheduleAt(std::function<void()> &&function,
+                                   aos::monotonic_clock::time_point time) {
+  functions_.insert(std::make_pair(time, std::move(function)));
+  timer_->Schedule(functions_.begin()->first);
+}
+
+void FunctionScheduler::RunFunctions(aos::monotonic_clock::time_point now) {
+  while (true) {
+    if (functions_.empty()) return;
+    if (functions_.begin()->first > now) {
+      break;
+    }
+    CHECK_EQ(functions_.begin()->first, now);
+
+    functions_.begin()->second();
+    functions_.erase(functions_.begin());
+  }
+  timer_->Schedule(functions_.begin()->first);
+}
+
+}  // namespace aos
diff --git a/aos/events/function_scheduler.h b/aos/events/function_scheduler.h
new file mode 100644
index 0000000..7a846c6
--- /dev/null
+++ b/aos/events/function_scheduler.h
@@ -0,0 +1,33 @@
+#ifndef AOS_EVENTS_FUNCTION_SCHEDULER_H_
+#define AOS_EVENTS_FUNCTION_SCHEDULER_H_
+
+#include <functional>
+#include <map>
+
+#include "aos/events/event_loop.h"
+#include "aos/time/time.h"
+
+namespace aos {
+
+// Simple class to call a function at a time with a timer.
+class FunctionScheduler {
+ public:
+  FunctionScheduler(aos::EventLoop *event_loop);
+
+  // Schedules the function to be run at the provided time.
+  void ScheduleAt(std::function<void()> &&function,
+                  aos::monotonic_clock::time_point time);
+
+ private:
+  void RunFunctions(aos::monotonic_clock::time_point now);
+
+  aos::EventLoop *event_loop_;
+  aos::TimerHandler *timer_;
+
+  std::multimap<aos::monotonic_clock::time_point, std::function<void()>>
+      functions_;
+};
+
+}  // namespace aos
+
+#endif  // AOS_EVENTS_FUNCTION_SCHEDULER_H_
diff --git a/aos/events/logging/BUILD b/aos/events/logging/BUILD
index a8979ee..eadefa8 100644
--- a/aos/events/logging/BUILD
+++ b/aos/events/logging/BUILD
@@ -807,6 +807,20 @@
 )
 
 aos_config(
+    name = "multinode_pingpong_reboot_reliable_only_config",
+    src = "multinode_pingpong_reboot_reliable_only.json",
+    flatbuffers = [
+        "//aos/events:ping_fbs",
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:remote_message_fbs",
+        "//aos/network:timestamp_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//aos/events:aos_config"],
+)
+
+aos_config(
     name = "multinode_pingpong_reboot_ooo_config",
     src = "multinode_pingpong_reboot_ooo.json",
     flatbuffers = [
@@ -895,6 +909,7 @@
         ":multinode_pingpong_combined_config",
         ":multinode_pingpong_pi3_pingpong_config",
         ":multinode_pingpong_reboot_ooo_config",
+        ":multinode_pingpong_reboot_reliable_only_config",
         ":multinode_pingpong_split3_config",
         ":multinode_pingpong_split4_config",
         ":multinode_pingpong_split4_mixed1_config",
diff --git a/aos/events/logging/file_operations.h b/aos/events/logging/file_operations.h
index 538ae60..0c2133d 100644
--- a/aos/events/logging/file_operations.h
+++ b/aos/events/logging/file_operations.h
@@ -16,7 +16,7 @@
  public:
   struct File {
     std::string name;
-    size_t size;
+    size_t size;  // bytes.
   };
 
   virtual ~FileOperations() = default;
diff --git a/aos/events/logging/log_namer.cc b/aos/events/logging/log_namer.cc
index 3bbca3b..3d52070 100644
--- a/aos/events/logging/log_namer.cc
+++ b/aos/events/logging/log_namer.cc
@@ -91,6 +91,10 @@
         monotonic_clock::max_time;
     state.oldest_logger_local_unreliable_monotonic_timestamp =
         monotonic_clock::max_time;
+    state.oldest_remote_reliable_monotonic_transmit_timestamp =
+        monotonic_clock::max_time;
+    state.oldest_local_reliable_monotonic_transmit_timestamp =
+        monotonic_clock::max_time;
   }
 
   state_[node_index_].boot_uuid = source_node_boot_uuid;
@@ -115,6 +119,7 @@
 void NewDataWriter::UpdateRemote(
     const size_t remote_node_index, const UUID &remote_node_boot_uuid,
     const monotonic_clock::time_point monotonic_remote_time,
+    const monotonic_clock::time_point monotonic_remote_transmit_time,
     const monotonic_clock::time_point monotonic_event_time, const bool reliable,
     monotonic_clock::time_point monotonic_timestamp_time) {
   // Trigger rotation if anything in the header changes.
@@ -140,9 +145,29 @@
         monotonic_clock::max_time;
     state.oldest_logger_local_unreliable_monotonic_timestamp =
         monotonic_clock::max_time;
+    state.oldest_remote_reliable_monotonic_transmit_timestamp =
+        monotonic_clock::max_time;
+    state.oldest_local_reliable_monotonic_transmit_timestamp =
+        monotonic_clock::max_time;
     rotate = true;
   }
 
+  if (monotonic_remote_transmit_time != monotonic_clock::min_time) {
+    if (state.oldest_remote_reliable_monotonic_transmit_timestamp >
+        monotonic_remote_transmit_time) {
+      VLOG(1) << name() << " Remote " << remote_node_index
+              << " oldest_remote_reliable_monotonic_transmit_timestamp updated "
+                 "from "
+              << state.oldest_remote_reliable_monotonic_transmit_timestamp
+              << " to " << monotonic_remote_transmit_time;
+      state.oldest_remote_reliable_monotonic_transmit_timestamp =
+          monotonic_remote_transmit_time;
+      state.oldest_local_reliable_monotonic_transmit_timestamp =
+          monotonic_event_time;
+      rotate = true;
+    }
+  }
+
   // Did the unreliable timestamps change?
   if (!reliable) {
     if (state.oldest_remote_unreliable_monotonic_timestamp >
@@ -395,7 +420,10 @@
   const UUID &source_node_boot_uuid = state[node_index].boot_uuid;
   const Node *const source_node =
       configuration::GetNode(configuration_, node_index);
-  CHECK_EQ(LogFileHeader::MiniReflectTypeTable()->num_elems, 35u);
+  CHECK_EQ(LogFileHeader::MiniReflectTypeTable()->num_elems, 37u)
+      << ": If you added new fields to the LogFileHeader table, don't forget "
+         "to add it below!";
+  ;
   flatbuffers::FlatBufferBuilder fbb;
   fbb.ForceDefaults(true);
 
@@ -494,6 +522,14 @@
       oldest_logger_local_unreliable_monotonic_timestamps_offset =
           fbb.CreateUninitializedVector(state.size(), &unused);
 
+  flatbuffers::Offset<flatbuffers::Vector<int64_t>>
+      oldest_remote_reliable_monotonic_transmit_timestamps_offset =
+          fbb.CreateUninitializedVector(state.size(), &unused);
+
+  flatbuffers::Offset<flatbuffers::Vector<int64_t>>
+      oldest_local_reliable_monotonic_transmit_timestamps_offset =
+          fbb.CreateUninitializedVector(state.size(), &unused);
+
   for (size_t i = 0; i < state.size(); ++i) {
     if (state[i].boot_uuid != UUID::Zero()) {
       boot_uuid_offsets.emplace_back(state[i].boot_uuid.PackString(&fbb));
@@ -517,6 +553,10 @@
                monotonic_clock::max_time);
       CHECK_EQ(state[i].oldest_logger_local_unreliable_monotonic_timestamp,
                monotonic_clock::max_time);
+      CHECK_EQ(state[i].oldest_remote_reliable_monotonic_transmit_timestamp,
+               monotonic_clock::max_time);
+      CHECK_EQ(state[i].oldest_local_reliable_monotonic_transmit_timestamp,
+               monotonic_clock::max_time);
     }
 
     flatbuffers::GetMutableTemporaryPointer(
@@ -567,6 +607,19 @@
                         .oldest_logger_local_unreliable_monotonic_timestamp
                         .time_since_epoch()
                         .count());
+
+    flatbuffers::GetMutableTemporaryPointer(
+        fbb, oldest_remote_reliable_monotonic_transmit_timestamps_offset)
+        ->Mutate(i, state[i]
+                        .oldest_remote_reliable_monotonic_transmit_timestamp
+                        .time_since_epoch()
+                        .count());
+    flatbuffers::GetMutableTemporaryPointer(
+        fbb, oldest_local_reliable_monotonic_transmit_timestamps_offset)
+        ->Mutate(i, state[i]
+                        .oldest_local_reliable_monotonic_transmit_timestamp
+                        .time_since_epoch()
+                        .count());
   }
 
   flatbuffers::Offset<
@@ -678,6 +731,12 @@
   log_file_header_builder
       .add_oldest_logger_local_unreliable_monotonic_timestamps(
           oldest_logger_local_unreliable_monotonic_timestamps_offset);
+  log_file_header_builder
+      .add_oldest_remote_reliable_monotonic_transmit_timestamps(
+          oldest_remote_reliable_monotonic_transmit_timestamps_offset);
+  log_file_header_builder
+      .add_oldest_local_reliable_monotonic_transmit_timestamps(
+          oldest_local_reliable_monotonic_transmit_timestamps_offset);
 
   log_file_header_builder.add_data_stored(data_stored_offset);
   fbb.FinishSizePrefixed(log_file_header_builder.Finish());
diff --git a/aos/events/logging/log_namer.h b/aos/events/logging/log_namer.h
index e0660eb..a0d5857 100644
--- a/aos/events/logging/log_namer.h
+++ b/aos/events/logging/log_namer.h
@@ -70,6 +70,7 @@
   // message is from.
   void UpdateRemote(size_t remote_node_index, const UUID &remote_node_boot_uuid,
                     monotonic_clock::time_point monotonic_remote_time,
+                    monotonic_clock::time_point monotonic_remote_transmit_time,
                     monotonic_clock::time_point monotonic_event_time,
                     bool reliable,
                     monotonic_clock::time_point monotonic_timestamp_time =
@@ -147,6 +148,17 @@
     monotonic_clock::time_point
         oldest_logger_local_unreliable_monotonic_timestamp =
             monotonic_clock::max_time;
+
+    // Transmit timestamp on the remote monotonic clock of the oldest message
+    // sent to node_index_.
+    monotonic_clock::time_point
+        oldest_remote_reliable_monotonic_transmit_timestamp =
+            monotonic_clock::max_time;
+    // Timestamp on the local monotonic clock of the message in
+    // oldest_remote_reliable_monotonic_transmit_timestamp.
+    monotonic_clock::time_point
+        oldest_local_reliable_monotonic_transmit_timestamp =
+            monotonic_clock::max_time;
   };
 
  private:
diff --git a/aos/events/logging/log_reader.cc b/aos/events/logging/log_reader.cc
index 5b437ba..df1ed8c 100644
--- a/aos/events/logging/log_reader.cc
+++ b/aos/events/logging/log_reader.cc
@@ -1177,7 +1177,9 @@
   // Make sure that things get destroyed in the correct order, rather than
   // relying on getting the order correct in the class definition.
   for (std::unique_ptr<State> &state : states_) {
-    state->Deregister();
+    if (state) {
+      state->Deregister();
+    }
   }
 
   event_loop_factory_unique_ptr_.reset();
@@ -1519,7 +1521,9 @@
   const RawSender::Error err = sender->Send(
       SharedSpan(timestamped_message.data, &timestamped_message.data->span),
       timestamped_message.monotonic_remote_time.time,
-      timestamped_message.realtime_remote_time, remote_queue_index,
+      timestamped_message.realtime_remote_time,
+      timestamped_message.monotonic_remote_transmit_time.time,
+      remote_queue_index,
       (channel_source_state_[timestamped_message.channel_index] != nullptr
            ? CHECK_NOTNULL(multinode_filters_)
                  ->boot_uuid(configuration::GetNodeIndex(
@@ -1612,6 +1616,10 @@
     message_header_builder.add_monotonic_remote_time(
         timestamped_message.monotonic_remote_time.time.time_since_epoch()
             .count());
+    message_header_builder.add_monotonic_remote_transmit_time(
+        timestamped_message.monotonic_remote_transmit_time.time
+            .time_since_epoch()
+            .count());
     message_header_builder.add_realtime_remote_time(
         timestamped_message.realtime_remote_time.time_since_epoch().count());
 
diff --git a/aos/events/logging/log_writer.cc b/aos/events/logging/log_writer.cc
index f840c22..c960ab9 100644
--- a/aos/events/logging/log_writer.cc
+++ b/aos/events/logging/log_writer.cc
@@ -41,23 +41,7 @@
   // from the channel index that the event loop uses to the channel index in
   // the config in the log file.
   event_loop_to_logged_channel_index_.resize(
-      event_loop->configuration()->channels()->size(), -1);
-  for (size_t event_loop_channel_index = 0;
-       event_loop_channel_index <
-       event_loop->configuration()->channels()->size();
-       ++event_loop_channel_index) {
-    const Channel *event_loop_channel =
-        event_loop->configuration()->channels()->Get(event_loop_channel_index);
-
-    const Channel *logged_channel = aos::configuration::GetChannel(
-        configuration_, event_loop_channel->name()->string_view(),
-        event_loop_channel->type()->string_view(), "", node_);
-
-    if (logged_channel != nullptr) {
-      event_loop_to_logged_channel_index_[event_loop_channel_index] =
-          configuration::ChannelIndex(configuration_, logged_channel);
-    }
-  }
+      event_loop->configuration()->channels()->size());
 
   // Map to match source channels with the timestamp logger, if the contents
   // should be reliable, and a list of all channels logged on it to be treated
@@ -92,6 +76,18 @@
         const Channel *const timestamp_logger_channel =
             finder.ForChannel(channel, connection);
 
+        const Channel *logged_channel = aos::configuration::GetChannel(
+            configuration_, channel->name()->string_view(),
+            channel->type()->string_view(), "", node_);
+        if (logged_channel == nullptr) {
+          // The channel doesn't exist in configuration_, so don't log it.
+          continue;
+        }
+        if (!should_log(logged_channel)) {
+          // The channel didn't pass our `should_log` check, so don't log it.
+          continue;
+        }
+
         auto it = timestamp_logger_channels.find(timestamp_logger_channel);
         if (it != timestamp_logger_channels.end()) {
           CHECK(!is_split);
@@ -120,47 +116,61 @@
     }
   }
 
-  for (size_t channel_index = 0;
-       channel_index < configuration_->channels()->size(); ++channel_index) {
-    const Channel *const config_channel =
-        configuration_->channels()->Get(channel_index);
+  for (size_t event_loop_channel_index = 0;
+       event_loop_channel_index <
+       event_loop->configuration()->channels()->size();
+       ++event_loop_channel_index) {
+    const Channel *event_loop_channel =
+        event_loop->configuration()->channels()->Get(event_loop_channel_index);
+
     // The MakeRawFetcher method needs a channel which is in the event loop
     // configuration() object, not the configuration_ object.  Go look that up
     // from the config.
-    const Channel *channel = aos::configuration::GetChannel(
-        event_loop_->configuration(), config_channel->name()->string_view(),
-        config_channel->type()->string_view(), "", event_loop_->node());
-    CHECK(channel != nullptr)
-        << ": Failed to look up channel "
-        << aos::configuration::CleanedChannelToString(config_channel);
-    if (!should_log(config_channel)) {
+    const Channel *logged_channel = aos::configuration::GetChannel(
+        configuration_, event_loop_channel->name()->string_view(),
+        event_loop_channel->type()->string_view(), "", node_);
+
+    if (logged_channel == nullptr) {
+      // The channel doesn't exist in configuration_, so don't log the
+      // timestamps.
+      continue;
+    }
+    if (!should_log(logged_channel)) {
+      // The channel didn't pass our `should_log` check, so don't log the
+      // timestamps.
       continue;
     }
 
+    const uint32_t logged_channel_index =
+        configuration::ChannelIndex(configuration_, logged_channel);
+
     FetcherStruct fs;
-    fs.channel_index = channel_index;
-    fs.channel = channel;
+    fs.logged_channel_index = logged_channel_index;
+    fs.event_loop_channel = event_loop_channel;
+
+    event_loop_to_logged_channel_index_[event_loop_channel_index] =
+        logged_channel_index;
 
     const bool is_local =
-        configuration::ChannelIsSendableOnNode(config_channel, node_);
+        configuration::ChannelIsSendableOnNode(logged_channel, node_);
 
     const bool is_readable =
-        configuration::ChannelIsReadableOnNode(config_channel, node_);
+        configuration::ChannelIsReadableOnNode(logged_channel, node_);
     const bool is_logged =
-        configuration::ChannelMessageIsLoggedOnNode(config_channel, node_);
+        configuration::ChannelMessageIsLoggedOnNode(logged_channel, node_);
     const bool log_message = is_logged && is_readable;
 
     bool log_delivery_times = false;
     if (configuration::NodesCount(configuration_) > 1u) {
       const aos::Connection *connection =
-          configuration::ConnectionToNode(config_channel, node_);
+          configuration::ConnectionToNode(logged_channel, node_);
 
       log_delivery_times = configuration::ConnectionDeliveryTimeIsLoggedOnNode(
           connection, event_loop_->node());
 
       CHECK_EQ(log_delivery_times,
                configuration::ConnectionDeliveryTimeIsLoggedOnNode(
-                   config_channel, node_, node_));
+                   logged_channel, node_, node_));
 
       if (connection) {
         fs.reliable_forwarding = (connection->time_to_live() == 0);
@@ -169,13 +179,14 @@
 
     // Now, detect a RemoteMessage timestamp logger where we should just log
     // the contents to a file directly.
-    const bool log_contents = timestamp_logger_channels.find(channel) !=
-                              timestamp_logger_channels.end();
+    const bool log_contents =
+        timestamp_logger_channels.find(event_loop_channel) !=
+        timestamp_logger_channels.end();
 
     if (log_message || log_delivery_times || log_contents) {
-      fs.fetcher = event_loop->MakeRawFetcher(channel);
+      fs.fetcher = event_loop->MakeRawFetcher(event_loop_channel);
       VLOG(1) << "Logging channel "
-              << configuration::CleanedChannelToString(channel);
+              << configuration::CleanedChannelToString(event_loop_channel);
 
       if (log_delivery_times) {
         VLOG(1) << "  Delivery times";
@@ -187,7 +198,7 @@
       if (log_message || log_delivery_times) {
         if (!is_local) {
           const Node *source_node = configuration::GetNode(
-              configuration_, channel->source_node()->string_view());
+              configuration_, event_loop_channel->source_node()->string_view());
           fs.data_node_index =
               configuration::GetNodeIndex(configuration_, source_node);
         }
@@ -203,9 +214,9 @@
       }
       if (log_contents) {
         VLOG(1) << "Timestamp logger channel "
-                << configuration::CleanedChannelToString(channel);
+                << configuration::CleanedChannelToString(event_loop_channel);
         auto timestamp_logger_channel_info =
-            timestamp_logger_channels.find(channel);
+            timestamp_logger_channels.find(event_loop_channel);
         CHECK(timestamp_logger_channel_info != timestamp_logger_channels.end());
         fs.timestamp_node = std::get<0>(timestamp_logger_channel_info->second);
         fs.reliable_contents =
@@ -276,14 +287,15 @@
 
   for (FetcherStruct &f : fetchers_) {
     if (f.wants_writer) {
-      f.writer = log_namer_->MakeWriter(f.channel);
+      f.writer = log_namer_->MakeWriter(f.event_loop_channel);
     }
     if (f.wants_timestamp_writer) {
-      f.timestamp_writer = log_namer_->MakeTimestampWriter(f.channel);
+      f.timestamp_writer =
+          log_namer_->MakeTimestampWriter(f.event_loop_channel);
     }
     if (f.wants_contents_writer) {
       f.contents_writer = log_namer_->MakeForwardedTimestampWriter(
-          f.channel, CHECK_NOTNULL(f.timestamp_node));
+          f.event_loop_channel, CHECK_NOTNULL(f.timestamp_node));
     }
   }
 
@@ -404,14 +416,15 @@
     // Create writers from the new namer
 
     if (f.wants_writer) {
-      f.writer = log_namer_->MakeWriter(f.channel);
+      f.writer = log_namer_->MakeWriter(f.event_loop_channel);
     }
     if (f.wants_timestamp_writer) {
-      f.timestamp_writer = log_namer_->MakeTimestampWriter(f.channel);
+      f.timestamp_writer =
+          log_namer_->MakeTimestampWriter(f.event_loop_channel);
     }
     if (f.wants_contents_writer) {
       f.contents_writer = log_namer_->MakeForwardedTimestampWriter(
-          f.channel, CHECK_NOTNULL(f.timestamp_node));
+          f.event_loop_channel, CHECK_NOTNULL(f.timestamp_node));
     }
 
     // Mark each channel with data as not written.  That triggers each channel
@@ -702,8 +715,8 @@
     // Write!
     const auto start = event_loop_->monotonic_now();
 
-    ContextDataCopier coppier(f.fetcher->context(), f.channel_index, f.log_type,
-                              event_loop_);
+    ContextDataCopier coppier(f.fetcher->context(), f.logged_channel_index,
+                              f.log_type, event_loop_);
 
     aos::monotonic_clock::time_point message_time =
         static_cast<int>(node_index_) != f.data_node_index
@@ -729,10 +742,11 @@
     timestamp_writer->UpdateRemote(
         f.data_node_index, f.fetcher->context().source_boot_uuid,
         f.fetcher->context().monotonic_remote_time,
+        f.fetcher->context().monotonic_remote_transmit_time,
         f.fetcher->context().monotonic_event_time, f.reliable_forwarding);
 
     const auto start = event_loop_->monotonic_now();
-    ContextDataCopier coppier(f.fetcher->context(), f.channel_index,
+    ContextDataCopier coppier(f.fetcher->context(), f.logged_channel_index,
                               LogType::kLogDeliveryTimeOnly, event_loop_);
 
     timestamp_writer->CopyTimestampMessage(
@@ -759,9 +773,11 @@
     CHECK(msg->has_boot_uuid()) << ": " << aos::FlatbufferToJson(msg);
     // Translate from the channel index that the event loop uses to the
     // channel index in the log file.
-    const int channel_index =
+    const std::optional<uint32_t> channel_index =
         event_loop_to_logged_channel_index_[msg->channel_index()];
 
+    CHECK(channel_index.has_value());
+
     const aos::monotonic_clock::time_point monotonic_timestamp_time =
         f.fetcher->context().monotonic_event_time;
 
@@ -781,11 +797,13 @@
         monotonic_clock::time_point(
             chrono::nanoseconds(msg->monotonic_remote_time())),
         monotonic_clock::time_point(
+            chrono::nanoseconds(msg->monotonic_remote_transmit_time())),
+        monotonic_clock::time_point(
             chrono::nanoseconds(msg->monotonic_sent_time())),
         reliable, monotonic_timestamp_time);
 
-    RemoteMessageCopier coppier(msg, channel_index, monotonic_timestamp_time,
-                                event_loop_);
+    RemoteMessageCopier coppier(msg, channel_index.value(),
+                                monotonic_timestamp_time, event_loop_);
 
     contents_writer->CopyRemoteTimestampMessage(
         &coppier, UUID::FromVector(msg->boot_uuid()), start,
@@ -896,7 +914,7 @@
   total_message_fetch_time_ += duration;
   if (duration > max_message_fetch_time_) {
     max_message_fetch_time_ = duration;
-    max_message_fetch_time_channel_ = fetcher->channel_index;
+    max_message_fetch_time_channel_ = fetcher->logged_channel_index;
     max_message_fetch_time_size_ = fetcher->fetcher->context().size;
   }
 }
@@ -910,13 +928,13 @@
   total_copy_bytes_ += fetcher.fetcher->context().size;
   if (duration > max_copy_time_) {
     max_copy_time_ = duration;
-    max_copy_time_channel_ = fetcher.channel_index;
+    max_copy_time_channel_ = fetcher.logged_channel_index;
     max_copy_time_size_ = fetcher.fetcher->context().size;
   }
   const auto log_delay = end - fetcher.fetcher->context().monotonic_event_time;
   if (log_delay > max_log_delay_) {
     max_log_delay_ = log_delay;
-    max_log_delay_channel_ = fetcher.channel_index;
+    max_log_delay_channel_ = fetcher.logged_channel_index;
   }
 }
 
diff --git a/aos/events/logging/log_writer.h b/aos/events/logging/log_writer.h
index eb69d7e..6091d7e 100644
--- a/aos/events/logging/log_writer.h
+++ b/aos/events/logging/log_writer.h
@@ -203,9 +203,13 @@
     std::unique_ptr<RawFetcher> fetcher;
     bool written = false;
 
-    // Channel index to log to.
-    int channel_index = -1;
-    const Channel *channel = nullptr;
+    // Index of the channel in the logged configuration (not necessarily the
+    // event loop configuration).
+    int logged_channel_index = -1;
+
+    // Channel from the event_loop configuration.
+    const Channel *event_loop_channel = nullptr;
+
     const Node *timestamp_node = nullptr;
 
     LogType log_type = LogType::kLogMessage;
@@ -245,7 +249,13 @@
 
   // Vector mapping from the channel index from the event loop to the logged
   // channel index.
-  std::vector<int> event_loop_to_logged_channel_index_;
+  // When using the constructor that allows manually specifying the
+  // configuration, that configuration may have different channels than the
+  // event loop's configuration. When there is a channel that is included in the
+  // event loop configuration but not in the specified configuration, the value
+  // in this mapping will be nullopt for that channel. Nullopt will result in
+  // that channel not being included in the output log's configuration or data.
+  std::vector<std::optional<uint32_t>> event_loop_to_logged_channel_index_;
 
   // Start/Restart write configuration into LogNamer space.
   std::string WriteConfiguration(LogNamer *log_namer);
diff --git a/aos/events/logging/logfile_sorting.cc b/aos/events/logging/logfile_sorting.cc
index 2eaf00b..4c859c2 100644
--- a/aos/events/logging/logfile_sorting.cc
+++ b/aos/events/logging/logfile_sorting.cc
@@ -48,7 +48,7 @@
 }
 
 bool ConfigOnly(const LogFileHeader *header) {
-  CHECK_EQ(LogFileHeader::MiniReflectTypeTable()->num_elems, 35u);
+  CHECK_EQ(LogFileHeader::MiniReflectTypeTable()->num_elems, 37u);
   if (header->has_monotonic_start_time()) return false;
   if (header->has_realtime_start_time()) return false;
   if (header->has_max_out_of_order_duration()) return false;
@@ -74,6 +74,10 @@
   if (header->has_oldest_local_unreliable_monotonic_timestamps()) return false;
   if (header->has_oldest_remote_reliable_monotonic_timestamps()) return false;
   if (header->has_oldest_local_reliable_monotonic_timestamps()) return false;
+  if (header->has_oldest_remote_reliable_monotonic_transmit_timestamps())
+    return false;
+  if (header->has_oldest_local_reliable_monotonic_transmit_timestamps())
+    return false;
   if (header->has_oldest_logger_remote_unreliable_monotonic_timestamps())
     return false;
   if (header->has_oldest_logger_local_unreliable_monotonic_timestamps())
@@ -711,17 +715,55 @@
             chrono::nanoseconds(
                 log_header->message().oldest_remote_monotonic_timestamps()->Get(
                     node_index)));
+
         const monotonic_clock::time_point
-            oldest_local_unreliable_monotonic_timestamp(chrono::nanoseconds(
+            oldest_local_reliable_monotonic_transmit_timestamp =
+                log_header->message()
+                        .has_oldest_local_reliable_monotonic_transmit_timestamps()
+                    ? monotonic_clock::time_point(chrono::nanoseconds(
+                          log_header->message()
+                              .oldest_local_reliable_monotonic_transmit_timestamps()
+                              ->Get(node_index)))
+                    : monotonic_clock::max_time;
+        const monotonic_clock::time_point
+            oldest_remote_reliable_monotonic_transmit_timestamp =
+                log_header->message()
+                        .has_oldest_remote_reliable_monotonic_transmit_timestamps()
+                    ? monotonic_clock::time_point(chrono::nanoseconds(
+                          log_header->message()
+                              .oldest_remote_reliable_monotonic_transmit_timestamps()
+                              ->Get(node_index)))
+                    : monotonic_clock::max_time;
+        monotonic_clock::time_point oldest_local_unreliable_monotonic_timestamp(
+            chrono::nanoseconds(
                 log_header->message()
                     .oldest_local_unreliable_monotonic_timestamps()
                     ->Get(node_index)));
-        const monotonic_clock::time_point
+        monotonic_clock::time_point
             oldest_remote_unreliable_monotonic_timestamp(chrono::nanoseconds(
                 log_header->message()
                     .oldest_remote_unreliable_monotonic_timestamps()
                     ->Get(node_index)));
 
+        // Treat transmit timestamps like unreliable timestamps.  Update
+        // oldest_remote_unreliable_monotonic_timestamp accordingly to keep the
+        // logic after it simple and similar.
+        if (oldest_remote_reliable_monotonic_transmit_timestamp <
+            oldest_remote_unreliable_monotonic_timestamp) {
+          VLOG(1)
+              << "Updating oldest_remote_unreliable_monotonic_timestamp from "
+              << oldest_remote_unreliable_monotonic_timestamp << " to "
+              << oldest_remote_reliable_monotonic_transmit_timestamp
+              << " and oldest_local_unreliable_monotonic_timestamp from "
+              << oldest_local_unreliable_monotonic_timestamp << " to "
+              << oldest_local_reliable_monotonic_transmit_timestamp;
+
+          oldest_remote_unreliable_monotonic_timestamp =
+              oldest_remote_reliable_monotonic_transmit_timestamp;
+          oldest_local_unreliable_monotonic_timestamp =
+              oldest_local_reliable_monotonic_transmit_timestamp;
+        }
+
         const monotonic_clock::time_point
             oldest_logger_local_unreliable_monotonic_timestamp =
                 log_header->message()
diff --git a/aos/events/logging/logfile_utils.cc b/aos/events/logging/logfile_utils.cc
index 0166b72..0676577 100644
--- a/aos/events/logging/logfile_utils.cc
+++ b/aos/events/logging/logfile_utils.cc
@@ -316,14 +316,17 @@
   message_header_builder.add_monotonic_sent_time(msg->monotonic_sent_time());
   message_header_builder.add_realtime_sent_time(msg->realtime_sent_time());
 
+  message_header_builder.add_monotonic_timestamp_time(
+      monotonic_timestamp_time.time_since_epoch().count());
+
+  message_header_builder.add_monotonic_remote_transmit_time(
+      msg->monotonic_remote_transmit_time());
+
   message_header_builder.add_monotonic_remote_time(
       msg->monotonic_remote_time());
   message_header_builder.add_realtime_remote_time(msg->realtime_remote_time());
   message_header_builder.add_remote_queue_index(msg->remote_queue_index());
 
-  message_header_builder.add_monotonic_timestamp_time(
-      monotonic_timestamp_time.time_since_epoch().count());
-
   return message_header_builder.Finish();
 }
 
@@ -345,119 +348,116 @@
       }
       // clang-format off
       // header:
-      //   +0x00 | 5C 00 00 00             | UOffset32  | 0x0000005C (92) Loc: +0x5C                | size prefix
+      //   +0x00 | 5C 00 00 00             | UOffset32 | 0x0000005C (92) Loc: +0x5C                | size prefix
+
       buffer = Push<flatbuffers::uoffset_t>(
           buffer, message_size - sizeof(flatbuffers::uoffset_t));
-      //   +0x04 | 20 00 00 00             | UOffset32  | 0x00000020 (32) Loc: +0x24                | offset to root table `aos.logger.MessageHeader`
-      buffer = Push<flatbuffers::uoffset_t>(buffer, 0x20);
+      //   +0x04 | 1C 00 00 00             | UOffset32 | 0x0000001C (28) Loc: +0x20                | offset to root table `aos.logger.MessageHeader`
+      buffer = Push<flatbuffers::uoffset_t>(buffer, 0x1C);
       [[fallthrough]];
     case 0x08u:
       if ((end_byte) == 0x08u) {
         break;
       }
       //
-      // padding:
-      //   +0x08 | 00 00 00 00 00 00       | uint8_t[6] | ......                                    | padding
-      buffer = Pad(buffer, 6);
-      //
       // vtable (aos.logger.MessageHeader):
-      //   +0x0E | 16 00                   | uint16_t   | 0x0016 (22)                               | size of this vtable
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x16);
+      //   +0x08 | 18 00                   | uint16_t  | 0x0018 (24)                               | size of this vtable
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x18);
+      //   +0x0A | 40 00                   | uint16_t  | 0x0040 (64)                               | size of referring table
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x40);
+      //   +0x0C | 3C 00                   | VOffset16 | 0x003C (60)                               | offset to field `channel_index` (id: 0)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x3c);
+      //   +0x0E | 30 00                   | VOffset16 | 0x0030 (48)                               | offset to field `monotonic_sent_time` (id: 1)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x30);
       [[fallthrough]];
     case 0x10u:
       if ((end_byte) == 0x10u) {
         break;
       }
-      //   +0x10 | 3C 00                   | uint16_t   | 0x003C (60)                               | size of referring table
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x3c);
-      //   +0x12 | 38 00                   | VOffset16  | 0x0038 (56)                               | offset to field `channel_index` (id: 0)
+      //   +0x10 | 28 00                   | VOffset16 | 0x0028 (40)                               | offset to field `realtime_sent_time` (id: 2)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x28);
+      //   +0x12 | 38 00                   | VOffset16 | 0x0038 (56)                               | offset to field `queue_index` (id: 3)
       buffer = Push<flatbuffers::voffset_t>(buffer, 0x38);
-      //   +0x14 | 2C 00                   | VOffset16  | 0x002C (44)                               | offset to field `monotonic_sent_time` (id: 1)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x2c);
-      //   +0x16 | 24 00                   | VOffset16  | 0x0024 (36)                               | offset to field `realtime_sent_time` (id: 2)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x24);
+      //   +0x14 | 00 00                   | VOffset16 | 0x0000 (0)                                | offset to field `data` (id: 4) <null> (Vector)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x00);
+      //   +0x16 | 10 00                   | VOffset16 | 0x0010 (16)                               | offset to field `monotonic_remote_time` (id: 5)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x10);
       [[fallthrough]];
     case 0x18u:
       if ((end_byte) == 0x18u) {
         break;
       }
-      //   +0x18 | 34 00                   | VOffset16  | 0x0034 (52)                               | offset to field `queue_index` (id: 3)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x34);
-      //   +0x1A | 00 00                   | VOffset16  | 0x0000 (0)                                | offset to field `data` (id: 4) <null> (Vector)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x00);
-      //   +0x1C | 1C 00                   | VOffset16  | 0x001C (28)                               | offset to field `monotonic_remote_time` (id: 5)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x1c);
-      //   +0x1E | 14 00                   | VOffset16  | 0x0014 (20)                               | offset to field `realtime_remote_time` (id: 6)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x14);
+      //   +0x18 | 08 00                   | VOffset16 | 0x0008 (8)                                | offset to field `realtime_remote_time` (id: 6)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x08);
+      //   +0x1A | 04 00                   | VOffset16 | 0x0004 (4)                                | offset to field `remote_queue_index` (id: 7)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x04);
+      //   +0x1C | 20 00                   | VOffset16 | 0x0020 (32)                               | offset to field `monotonic_timestamp_time` (id: 8)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x20);
+      //   +0x1E | 18 00                   | VOffset16 | 0x0018 (24)                               | offset to field `monotonic_remote_transmit_time` (id: 9)
+      buffer = Push<flatbuffers::voffset_t>(buffer, 0x18);
       [[fallthrough]];
     case 0x20u:
       if ((end_byte) == 0x20u) {
         break;
       }
-      //   +0x20 | 10 00                   | VOffset16  | 0x0010 (16)                               | offset to field `remote_queue_index` (id: 7)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x10);
-      //   +0x22 | 04 00                   | VOffset16  | 0x0004 (4)                                | offset to field `monotonic_timestamp_time` (id: 8)
-      buffer = Push<flatbuffers::voffset_t>(buffer, 0x04);
-      //
+
+
       // root_table (aos.logger.MessageHeader):
-      //   +0x24 | 16 00 00 00             | SOffset32  | 0x00000016 (22) Loc: +0x0E                | offset to vtable
-      buffer = Push<flatbuffers::uoffset_t>(buffer, 0x16);
+      //   +0x20 | 18 00 00 00             | SOffset32 | 0x00000018 (24) Loc: +0x08                | offset to vtable
+      buffer = Push<flatbuffers::uoffset_t>(buffer, 0x18);
+      //   +0x24 | 8B 00 00 00             | uint32_t  | 0x0000008B (139)                          | table field `remote_queue_index` (UInt)
+      buffer = Push<uint32_t>(buffer, msg->remote_queue_index());
       [[fallthrough]];
     case 0x28u:
       if ((end_byte) == 0x28u) {
         break;
       }
-      //   +0x28 | F6 0B D8 11 A4 A8 B1 71 | int64_t    | 0x71B1A8A411D80BF6 (8192514619791117302)  | table field `monotonic_timestamp_time` (Long)
-      buffer = Push<int64_t>(buffer,
-                             monotonic_timestamp_time.time_since_epoch().count());
+      //   +0x28 | D4 C9 48 86 92 8B 6A AF | int64_t   | 0xAF6A8B928648C9D4 (-5806675308106429996) | table field `realtime_remote_time` (Long)
+      buffer = Push<int64_t>(buffer, msg->realtime_remote_time());
       [[fallthrough]];
     case 0x30u:
       if ((end_byte) == 0x30u) {
         break;
       }
-      //   +0x30 | 00 00 00 00             | uint8_t[4] | ....                                      | padding
-      // TODO(austin): Can we re-arrange the order to ditch the padding?
-      // (Answer is yes, but what is the impact elsewhere?  It will change the
-      // binary format)
-      buffer = Pad(buffer, 4);
-      //   +0x34 | 75 00 00 00             | uint32_t   | 0x00000075 (117)                          | table field `remote_queue_index` (UInt)
-      buffer = Push<uint32_t>(buffer, msg->remote_queue_index());
+      //    +0x30 | 65 B1 32 50 FE 54 50 6B | int64_t   | 0x6B5054FE5032B165 (7732774011439067493)  | table field `monotonic_remote_time` (Long)
+      buffer = Push<int64_t>(buffer, msg->monotonic_remote_time());
       [[fallthrough]];
     case 0x38u:
       if ((end_byte) == 0x38u) {
         break;
       }
-      //   +0x38 | AA B0 43 0A 35 BE FA D2 | int64_t    | 0xD2FABE350A43B0AA (-3244071446552268630) | table field `realtime_remote_time` (Long)
-      buffer = Push<int64_t>(buffer, msg->realtime_remote_time());
+      //   +0x38 | EA 4D CC E0 FC 20 86 71 | int64_t   | 0x718620FCE0CC4DEA (8180262043640417770)  | table field `monotonic_remote_transmit_time` (Long)
+      buffer = Push<int64_t>(buffer, msg->monotonic_remote_transmit_time());
       [[fallthrough]];
     case 0x40u:
       if ((end_byte) == 0x40u) {
         break;
       }
-      //   +0x40 | D5 40 30 F3 C1 A7 26 1D | int64_t    | 0x1D26A7C1F33040D5 (2100550727665467605)  | table field `monotonic_remote_time` (Long)
-      buffer = Push<int64_t>(buffer, msg->monotonic_remote_time());
+      //   +0x40 | 8E 59 CF 88 9D DF 02 07 | int64_t   | 0x0702DF9D88CF598E (505211975917066638)   | table field `monotonic_timestamp_time` (Long)
+      buffer = Push<int64_t>(buffer,
+                             monotonic_timestamp_time.time_since_epoch().count());
       [[fallthrough]];
     case 0x48u:
       if ((end_byte) == 0x48u) {
         break;
       }
-      //   +0x48 | 5B 25 32 A1 4A E8 46 CA | int64_t    | 0xCA46E84AA132255B (-3871151422448720549) | table field `realtime_sent_time` (Long)
+      //   +0x48 | 14 D5 A7 D8 B2 E4 EF 89 | int64_t   | 0x89EFE4B2D8A7D514 (-8507329714289388268) | table field `realtime_sent_time` (Long)
       buffer = Push<int64_t>(buffer, msg->realtime_sent_time());
       [[fallthrough]];
     case 0x50u:
       if ((end_byte) == 0x50u) {
         break;
       }
-      //   +0x50 | 49 7D 45 1F 8C 36 6B A3 | int64_t    | 0xA36B368C1F457D49 (-6671178447571288759) | table field `monotonic_sent_time` (Long)
+      //    +0x50 | 19 7D 7F EF 86 8D 92 65 | int64_t   | 0x65928D86EF7F7D19 (7319067955113721113)  | table field `monotonic_sent_time` (Long)
       buffer = Push<int64_t>(buffer, msg->monotonic_sent_time());
       [[fallthrough]];
     case 0x58u:
       if ((end_byte) == 0x58u) {
         break;
       }
-      //   +0x58 | 33 00 00 00             | uint32_t   | 0x00000033 (51)                           | table field `queue_index` (UInt)
+      //   +0x58 | FC 00 00 00             | uint32_t  | 0x000000FC (252)                          | table field `queue_index` (UInt)
       buffer = Push<uint32_t>(buffer, msg->queue_index());
-      //   +0x5C | 76 00 00 00             | uint32_t   | 0x00000076 (118)                          | table field `channel_index` (UInt)
+      //   +0x5C | 9C 00 00 00             | uint32_t  | 0x0000009C (156)                          | table field `channel_index` (UInt)
       buffer = Push<uint32_t>(buffer, channel_index);
       // clang-format on
       [[fallthrough]];
@@ -477,7 +477,6 @@
 
   switch (log_type) {
     case LogType::kLogMessage:
-    case LogType::kLogMessageAndDeliveryTime:
     case LogType::kLogRemoteMessage:
       // Since the timestamps are 8 byte aligned, we are going to end up adding
       // padding in the middle of the message to pad everything out to 8 byte
@@ -525,6 +524,8 @@
           context.monotonic_remote_time.time_since_epoch().count());
       message_header_builder.add_realtime_remote_time(
           context.realtime_remote_time.time_since_epoch().count());
+      message_header_builder.add_monotonic_remote_transmit_time(
+          context.monotonic_remote_transmit_time.time_since_epoch().count());
       message_header_builder.add_remote_queue_index(context.remote_queue_index);
       break;
 
@@ -536,20 +537,6 @@
       message_header_builder.add_realtime_sent_time(
           context.realtime_event_time.time_since_epoch().count());
       break;
-
-    case LogType::kLogMessageAndDeliveryTime:
-      message_header_builder.add_queue_index(context.queue_index);
-      message_header_builder.add_remote_queue_index(context.remote_queue_index);
-      message_header_builder.add_monotonic_sent_time(
-          context.monotonic_event_time.time_since_epoch().count());
-      message_header_builder.add_realtime_sent_time(
-          context.realtime_event_time.time_since_epoch().count());
-      message_header_builder.add_monotonic_remote_time(
-          context.monotonic_remote_time.time_since_epoch().count());
-      message_header_builder.add_realtime_remote_time(
-          context.realtime_remote_time.time_since_epoch().count());
-      message_header_builder.add_data(data_offset);
-      break;
   }
 
   return message_header_builder.Finish();
@@ -580,42 +567,20 @@
       return
           // Root table size + offset.
           sizeof(flatbuffers::uoffset_t) * 2 +
-          // 6 padding bytes to pad the header out properly.
-          4 +
           // vtable header (size + size of table)
           sizeof(flatbuffers::voffset_t) * 2 +
           // offsets to all the fields.
-          sizeof(flatbuffers::voffset_t) * 8 +
+          sizeof(flatbuffers::voffset_t) * 10 +
           // pointer to vtable
           sizeof(flatbuffers::soffset_t) +
           // remote_queue_index
           sizeof(uint32_t) +
           // realtime_remote_time, monotonic_remote_time, realtime_sent_time,
           // monotonic_sent_time
-          sizeof(int64_t) * 4 +
+          sizeof(int64_t) * 5 +
           // queue_index, channel_index
           sizeof(uint32_t) * 2;
 
-    case LogType::kLogMessageAndDeliveryTime:
-      return
-          // Root table size + offset.
-          sizeof(flatbuffers::uoffset_t) * 2 +
-          // 4 padding bytes to pad the header out properly.
-          4 +
-          // vtable header (size + size of table)
-          sizeof(flatbuffers::voffset_t) * 2 +
-          // offsets to all the fields.
-          sizeof(flatbuffers::voffset_t) * 8 +
-          // pointer to vtable
-          sizeof(flatbuffers::soffset_t) +
-          // pointer to data
-          sizeof(flatbuffers::uoffset_t) +
-          // realtime_remote_time, monotonic_remote_time, realtime_sent_time,
-          // monotonic_sent_time
-          sizeof(int64_t) * 4 +
-          // remote_queue_index, queue_index, channel_index
-          sizeof(uint32_t) * 3;
-
     case LogType::kLogRemoteMessage:
       return
           // Root table size + offset.
@@ -648,7 +613,6 @@
       return PackMessageHeaderSize(log_type);
 
     case LogType::kLogMessage:
-    case LogType::kLogMessageAndDeliveryTime:
     case LogType::kLogRemoteMessage:
       return PackMessageHeaderSize(log_type) +
              // Vector...
@@ -822,10 +786,10 @@
           }
           // clang-format off
           // header:
-          //   +0x00 | 4C 00 00 00             | UOffset32  | 0x0000004C (76) Loc: +0x4C                | size prefix
+          //   +0x00 | 54 00 00 00             | UOffset32 | 0x00000054 (84) Loc: +0x54                | size prefix
           buffer = Push<flatbuffers::uoffset_t>(
               buffer, message_size - sizeof(flatbuffers::uoffset_t));
-          //   +0x04 | 1C 00 00 00             | UOffset32  | 0x0000001C (28) Loc: +0x20                | offset to root table `aos.logger.MessageHeader`
+          //   +0x04 | 1C 00 00 00             | UOffset32 | 0x0000001C (28) Loc: +0x20                | offset to root table `aos.logger.MessageHeader`
           buffer = Push<flatbuffers::uoffset_t>(buffer, 0x1c);
 
           [[fallthrough]];
@@ -834,254 +798,98 @@
             break;
           }
           //
-          // padding:
-          //   +0x08 | 00 00 00 00             | uint8_t[4] | ....                                      | padding
-          buffer = Pad(buffer, 4);
-          //
           // vtable (aos.logger.MessageHeader):
-          //   +0x0C | 14 00                   | uint16_t   | 0x0014 (20)                               | size of this vtable
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x14);
-          //   +0x0E | 30 00                   | uint16_t   | 0x0030 (48)                               | size of referring table
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x30);
-          [[fallthrough]];
-        case 0x10u:
-          if ((end_byte) == 0x10u) {
-            break;
-          }
-          //   +0x10 | 2C 00                   | VOffset16  | 0x002C (44)                               | offset to field `channel_index` (id: 0)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x2c);
-          //   +0x12 | 20 00                   | VOffset16  | 0x0020 (32)                               | offset to field `monotonic_sent_time` (id: 1)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x20);
-          //   +0x14 | 18 00                   | VOffset16  | 0x0018 (24)                               | offset to field `realtime_sent_time` (id: 2)
+          //   +0x08 | 18 00                   | uint16_t  | 0x0018 (24)                               | size of this vtable
           buffer = Push<flatbuffers::voffset_t>(buffer, 0x18);
-          //   +0x16 | 28 00                   | VOffset16  | 0x0028 (40)                               | offset to field `queue_index` (id: 3)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x28);
-          [[fallthrough]];
-        case 0x18u:
-          if ((end_byte) == 0x18u) {
-            break;
-          }
-          //   +0x18 | 00 00                   | VOffset16  | 0x0000 (0)                                | offset to field `data` (id: 4) <null> (Vector)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x00);
-          //   +0x1A | 10 00                   | VOffset16  | 0x0010 (16)                               | offset to field `monotonic_remote_time` (id: 5)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x10);
-          //   +0x1C | 08 00                   | VOffset16  | 0x0008 (8)                                | offset to field `realtime_remote_time` (id: 6)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x08);
-          //   +0x1E | 04 00                   | VOffset16  | 0x0004 (4)                                | offset to field `remote_queue_index` (id: 7)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x04);
-          [[fallthrough]];
-        case 0x20u:
-          if ((end_byte) == 0x20u) {
-            break;
-          }
-          //
-          // root_table (aos.logger.MessageHeader):
-          //   +0x20 | 14 00 00 00             | SOffset32  | 0x00000014 (20) Loc: +0x0C                | offset to vtable
-          buffer = Push<flatbuffers::uoffset_t>(buffer, 0x14);
-          //   +0x24 | 69 00 00 00             | uint32_t   | 0x00000069 (105)                          | table field `remote_queue_index` (UInt)
-          buffer = Push<uint32_t>(buffer, context.remote_queue_index);
-          [[fallthrough]];
-        case 0x28u:
-          if ((end_byte) == 0x28u) {
-            break;
-          }
-          //   +0x28 | C6 85 F1 AB 83 B5 CD EB | int64_t    | 0xEBCDB583ABF185C6 (-1455307527440726586) | table field `realtime_remote_time` (Long)
-          buffer = Push<int64_t>(buffer, context.realtime_remote_time.time_since_epoch().count());
-          [[fallthrough]];
-        case 0x30u:
-          if ((end_byte) == 0x30u) {
-            break;
-          }
-          //   +0x30 | 47 24 D3 97 1E 42 2D 99 | int64_t    | 0x992D421E97D32447 (-7409193112790948793) | table field `monotonic_remote_time` (Long)
-          buffer = Push<int64_t>(buffer, context.monotonic_remote_time.time_since_epoch().count());
-          [[fallthrough]];
-        case 0x38u:
-          if ((end_byte) == 0x38u) {
-            break;
-          }
-          //   +0x38 | C8 B9 A7 AB 79 F2 CD 60 | int64_t    | 0x60CDF279ABA7B9C8 (6975498002251626952)  | table field `realtime_sent_time` (Long)
-          buffer = Push<int64_t>(buffer, context.realtime_event_time.time_since_epoch().count());
-          [[fallthrough]];
-        case 0x40u:
-          if ((end_byte) == 0x40u) {
-            break;
-          }
-          //   +0x40 | EA 8F 2A 0F AF 01 7A AB | int64_t    | 0xAB7A01AF0F2A8FEA (-6090553694679822358) | table field `monotonic_sent_time` (Long)
-          buffer = Push<int64_t>(buffer, context.monotonic_event_time.time_since_epoch().count());
-          [[fallthrough]];
-        case 0x48u:
-          if ((end_byte) == 0x48u) {
-            break;
-          }
-          //   +0x48 | F5 00 00 00             | uint32_t   | 0x000000F5 (245)                          | table field `queue_index` (UInt)
-          buffer = Push<uint32_t>(buffer, context.queue_index);
-          //   +0x4C | 88 00 00 00             | uint32_t   | 0x00000088 (136)                          | table field `channel_index` (UInt)
-          buffer = Push<uint32_t>(buffer, channel_index);
-
-          // clang-format on
-      }
-      break;
-
-    case LogType::kLogMessageAndDeliveryTime:
-      switch (start_byte) {
-        case 0x00u:
-          if ((end_byte) == 0x00u) {
-            break;
-          }
-          // clang-format off
-          // header:
-          //   +0x00 | 5C 00 00 00             | UOffset32  | 0x0000005C (92) Loc: +0x5C                | size prefix
-          buffer = Push<flatbuffers::uoffset_t>(
-              buffer, message_size - sizeof(flatbuffers::uoffset_t));
-          //   +0x04 | 1C 00 00 00             | UOffset32  | 0x0000001C (28) Loc: +0x20                | offset to root table `aos.logger.MessageHeader`
-          buffer = Push<flatbuffers::uoffset_t>(buffer, 0x1c);
-          [[fallthrough]];
-        case 0x08u:
-          if ((end_byte) == 0x08u) {
-            break;
-          }
-          //
-          // padding:
-          //   +0x08 | 00 00 00 00             | uint8_t[4] | ....                                      | padding
-          buffer = Pad(buffer, 4);
-          //
-          // vtable (aos.logger.MessageHeader):
-          //   +0x0C | 14 00                   | uint16_t   | 0x0014 (20)                               | size of this vtable
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x14);
-          //   +0x0E | 34 00                   | uint16_t   | 0x0034 (52)                               | size of referring table
+          //   +0x0A | 38 00                   | uint16_t  | 0x0038 (56)                               | size of referring table
+          buffer = Push<flatbuffers::voffset_t>(buffer, 0x38);
+          //   +0x0C | 34 00                   | VOffset16 | 0x0034 (52)                               | offset to field `channel_index` (id: 0)
           buffer = Push<flatbuffers::voffset_t>(buffer, 0x34);
+          //   +0x0E | 28 00                   | VOffset16 | 0x0028 (40)                               | offset to field `monotonic_sent_time` (id: 1)
+          buffer = Push<flatbuffers::voffset_t>(buffer, 0x28);
           [[fallthrough]];
         case 0x10u:
           if ((end_byte) == 0x10u) {
             break;
           }
-          //   +0x10 | 30 00                   | VOffset16  | 0x0030 (48)                               | offset to field `channel_index` (id: 0)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x30);
-          //   +0x12 | 20 00                   | VOffset16  | 0x0020 (32)                               | offset to field `monotonic_sent_time` (id: 1)
+          //   +0x10 | 20 00                   | VOffset16 | 0x0020 (32)                               | offset to field `realtime_sent_time` (id: 2)
           buffer = Push<flatbuffers::voffset_t>(buffer, 0x20);
-          //   +0x14 | 18 00                   | VOffset16  | 0x0018 (24)                               | offset to field `realtime_sent_time` (id: 2)
+          //   +0x12 | 30 00                   | VOffset16 | 0x0030 (48)                               | offset to field `queue_index` (id: 3)
+          buffer = Push<flatbuffers::voffset_t>(buffer, 0x30);
+          //   +0x14 | 00 00                   | VOffset16 | 0x0000 (0)                                | offset to field `data` (id: 4) <null> (Vector)
+          buffer = Push<flatbuffers::voffset_t>(buffer, 0x00);
+          //   +0x16 | 18 00                   | VOffset16 | 0x0018 (24)                               | offset to field `monotonic_remote_time` (id: 5)
           buffer = Push<flatbuffers::voffset_t>(buffer, 0x18);
-          //   +0x16 | 2C 00                   | VOffset16  | 0x002C (44)                               | offset to field `queue_index` (id: 3)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x2c);
           [[fallthrough]];
         case 0x18u:
           if ((end_byte) == 0x18u) {
             break;
           }
-          //   +0x18 | 04 00                   | VOffset16  | 0x0004 (4)                                | offset to field `data` (id: 4)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x04);
-          //   +0x1A | 10 00                   | VOffset16  | 0x0010 (16)                               | offset to field `monotonic_remote_time` (id: 5)
+          //   +0x18 | 10 00                   | VOffset16 | 0x0010 (16)                               | offset to field `realtime_remote_time` (id: 6)
           buffer = Push<flatbuffers::voffset_t>(buffer, 0x10);
-          //   +0x1C | 08 00                   | VOffset16  | 0x0008 (8)                                | offset to field `realtime_remote_time` (id: 6)
+          //   +0x1A | 04 00                   | VOffset16 | 0x0004 (4)                                | offset to field `remote_queue_index` (id: 7)
+          buffer = Push<flatbuffers::voffset_t>(buffer, 0x04);
+          //   +0x1C | 00 00                   | VOffset16 | 0x0000 (0)                                | offset to field `monotonic_timestamp_time` (id: 8) <defaults to -9223372036854775808> (Long)
+          buffer = Push<flatbuffers::voffset_t>(buffer, 0x00);
+          //   +0x1E | 08 00                   | VOffset16 | 0x0008 (8)                                | offset to field `monotonic_remote_transmit_time` (id: 9)
           buffer = Push<flatbuffers::voffset_t>(buffer, 0x08);
-          //   +0x1E | 28 00                   | VOffset16  | 0x0028 (40)                               | offset to field `remote_queue_index` (id: 7)
-          buffer = Push<flatbuffers::voffset_t>(buffer, 0x28);
           [[fallthrough]];
         case 0x20u:
           if ((end_byte) == 0x20u) {
             break;
           }
-          //
           // root_table (aos.logger.MessageHeader):
-          //   +0x20 | 14 00 00 00             | SOffset32  | 0x00000014 (20) Loc: +0x0C                | offset to vtable
-          buffer = Push<flatbuffers::uoffset_t>(buffer, 0x14);
-          //   +0x24 | 30 00 00 00             | UOffset32  | 0x00000030 (48) Loc: +0x54                | offset to field `data` (vector)
-          buffer = Push<flatbuffers::uoffset_t>(buffer, 0x30);
+          //   +0x20 | 18 00 00 00             | SOffset32 | 0x00000018 (24) Loc: +0x08                | offset to vtable
+          buffer = Push<flatbuffers::uoffset_t>(buffer, 0x18);
+          //   +0x24 | 3F 9A 69 37             | uint32_t  | 0x37699A3F (929667647)                    | table field `remote_queue_index` (UInt)
+          buffer = Push<uint32_t>(buffer, context.remote_queue_index);
           [[fallthrough]];
         case 0x28u:
           if ((end_byte) == 0x28u) {
             break;
           }
-          //   +0x28 | C4 C8 87 BF 40 6C 1F 29 | int64_t    | 0x291F6C40BF87C8C4 (2963206105180129476)  | table field `realtime_remote_time` (Long)
-          buffer = Push<int64_t>(buffer, context.realtime_remote_time.time_since_epoch().count());
+          //   +0x28 | 00 00 00 00 00 00 00 80 | int64_t   | 0x8000000000000000 (-9223372036854775808) | table field `monotonic_remote_transmit_time` (Long)
+          buffer = Push<int64_t>(buffer, context.monotonic_remote_transmit_time.time_since_epoch().count());
           [[fallthrough]];
         case 0x30u:
           if ((end_byte) == 0x30u) {
             break;
           }
-          //   +0x30 | 0F 00 26 FD D2 6D C0 1F | int64_t    | 0x1FC06DD2FD26000F (2287949363661897743)  | table field `monotonic_remote_time` (Long)
-          buffer = Push<int64_t>(buffer, context.monotonic_remote_time.time_since_epoch().count());
+          //   +0x30 | 1D CE 4A 38 54 33 C9 F8 | int64_t   | 0xF8C93354384ACE1D (-519827845169885667)  | table field `realtime_remote_time` (Long)
+          buffer = Push<int64_t>(buffer, context.realtime_remote_time.time_since_epoch().count());
           [[fallthrough]];
         case 0x38u:
           if ((end_byte) == 0x38u) {
             break;
           }
-          //   +0x38 | 29 75 09 C0 73 73 BF 88 | int64_t    | 0x88BF7373C0097529 (-8593022623019338455) | table field `realtime_sent_time` (Long)
-          buffer = Push<int64_t>(buffer, context.realtime_event_time.time_since_epoch().count());
+          //   +0x38 | FE EA DF 1D C7 3F C6 03 | int64_t   | 0x03C63FC71DDFEAFE (271974951934749438)   | table field `monotonic_remote_time` (Long)
+          buffer = Push<int64_t>(buffer, context.monotonic_remote_time.time_since_epoch().count());
           [[fallthrough]];
         case 0x40u:
           if ((end_byte) == 0x40u) {
             break;
           }
-          //   +0x40 | 6D 8A AE 04 50 25 9C E9 | int64_t    | 0xE99C255004AE8A6D (-1613373540899321235) | table field `monotonic_sent_time` (Long)
-          buffer = Push<int64_t>(buffer, context.monotonic_event_time.time_since_epoch().count());
+          //   +0x40 | 4E 0C 96 6E FB B5 CE 12 | int64_t   | 0x12CEB5FB6E960C4E (1355220629381844046)  | table field `realtime_sent_time` (Long)
+          buffer = Push<int64_t>(buffer, context.realtime_event_time.time_since_epoch().count());
           [[fallthrough]];
         case 0x48u:
           if ((end_byte) == 0x48u) {
             break;
           }
-          //   +0x48 | 47 00 00 00             | uint32_t   | 0x00000047 (71)                           | table field `remote_queue_index` (UInt)
-          buffer = Push<uint32_t>(buffer, context.remote_queue_index);
-          //   +0x4C | 4C 00 00 00             | uint32_t   | 0x0000004C (76)                           | table field `queue_index` (UInt)
-          buffer = Push<uint32_t>(buffer, context.queue_index);
+          //   +0x48 | 51 56 56 F9 0A 0B 0F 12 | int64_t   | 0x120F0B0AF9565651 (1301270959094126161)  | table field `monotonic_sent_time` (Long)
+          buffer = Push<int64_t>(buffer, context.monotonic_event_time.time_since_epoch().count());
           [[fallthrough]];
         case 0x50u:
           if ((end_byte) == 0x50u) {
             break;
           }
-          //   +0x50 | 72 00 00 00             | uint32_t   | 0x00000072 (114)                          | table field `channel_index` (UInt)
+          //   +0x50 | 0C A5 42 18             | uint32_t  | 0x1842A50C (407020812)                    | table field `queue_index` (UInt)
+          buffer = Push<uint32_t>(buffer, context.queue_index);
+          //   +0x54 | 87 10 7C D7             | uint32_t  | 0xD77C1087 (3615232135)                   | table field `channel_index` (UInt)
           buffer = Push<uint32_t>(buffer, channel_index);
-          //
-          // vector (aos.logger.MessageHeader.data):
-          //   +0x54 | 07 00 00 00             | uint32_t   | 0x00000007 (7)                            | length of vector (# items)
-          buffer = Push<flatbuffers::uoffset_t>(buffer, context.size);
-          [[fallthrough]];
-        case 0x58u:
-          if ((end_byte) == 0x58u) {
-            break;
-          }
-          [[fallthrough]];
-        default:
-          //   +0x58 | B1                      | uint8_t    | 0xB1 (177)                                | value[0]
-          //   +0x59 | 4A                      | uint8_t    | 0x4A (74)                                 | value[1]
-          //   +0x5A | 50                      | uint8_t    | 0x50 (80)                                 | value[2]
-          //   +0x5B | 24                      | uint8_t    | 0x24 (36)                                 | value[3]
-          //   +0x5C | AF                      | uint8_t    | 0xAF (175)                                | value[4]
-          //   +0x5D | C8                      | uint8_t    | 0xC8 (200)                                | value[5]
-          //   +0x5E | D5                      | uint8_t    | 0xD5 (213)                                | value[6]
-          //
-          // padding:
-          //   +0x5F | 00                      | uint8_t[1] | .                                         | padding
+
           // clang-format on
-
-          if (start_byte <= 0x58 && end_byte == message_size) {
-            // The easy one, slap it all down.
-            buffer = PushBytes(buffer, context.data, context.size);
-            buffer =
-                Pad(buffer, ((context.size + 7) & 0xfffffff8u) - context.size);
-          } else {
-            const size_t data_start_byte =
-                start_byte < 0x58 ? 0x0u : (start_byte - 0x58);
-            const size_t data_end_byte = end_byte - 0x58;
-            const size_t padded_size = ((context.size + 7) & 0xfffffff8u);
-            if (data_start_byte < padded_size) {
-              buffer = PushBytes(
-                  buffer,
-                  reinterpret_cast<const uint8_t *>(context.data) +
-                      data_start_byte,
-                  std::min(context.size, data_end_byte) - data_start_byte);
-              if (data_end_byte == padded_size) {
-                // We can only pad the last 7 bytes, so this only gets written
-                // if we write the last byte.
-                buffer = Pad(buffer,
-                             ((context.size + 7) & 0xfffffff8u) - context.size);
-              }
-            }
-          }
-
-          break;
       }
-
       break;
 
     case LogType::kLogRemoteMessage:
@@ -1559,6 +1367,9 @@
     realtime_remote_time = realtime_clock::time_point(
         chrono::nanoseconds(message.realtime_remote_time()));
   }
+  aos::monotonic_clock::time_point monotonic_remote_transmit_time =
+      aos::monotonic_clock::time_point(
+          std::chrono::nanoseconds(message.monotonic_remote_transmit_time()));
 
   std::optional<uint32_t> remote_queue_index;
   if (message.has_remote_queue_index()) {
@@ -1572,7 +1383,7 @@
       realtime_clock::time_point(
           chrono::nanoseconds(message.realtime_sent_time())),
       message.queue_index(), monotonic_remote_time, realtime_remote_time,
-      remote_queue_index,
+      monotonic_remote_transmit_time, remote_queue_index,
       monotonic_clock::time_point(
           std::chrono::nanoseconds(message.monotonic_timestamp_time())),
       message.has_monotonic_timestamp_time(), span);
@@ -1780,6 +1591,10 @@
   if (msg.realtime_remote_time != realtime_clock::min_time) {
     os << ", .realtime_remote_time=" << msg.realtime_remote_time;
   }
+  if (msg.monotonic_remote_transmit_time != BootTimestamp::min_time()) {
+    os << ", .monotonic_remote_transmit_time="
+       << msg.monotonic_remote_transmit_time;
+  }
   if (msg.monotonic_timestamp_time != BootTimestamp::min_time()) {
     os << ", .monotonic_timestamp_time=" << msg.monotonic_timestamp_time;
   }
@@ -1824,6 +1639,7 @@
       size_t monotonic_remote_boot = 0xffffff;
 
       if (msg->monotonic_remote_time.has_value()) {
+        CHECK_LT(msg->channel_index, source_node_index_.size());
         const Node *node = parts().config->nodes()->Get(
             source_node_index_[msg->channel_index]);
 
@@ -2204,6 +2020,7 @@
       queue_timestamps_ran_ = true;
       return;
     }
+    CHECK_LT(msg->channel_index, source_node.size());
     if (source_node[msg->channel_index] != static_cast<size_t>(node())) {
       timestamp_messages_.emplace_back(TimestampedMessage{
           .channel_index = msg->channel_index,
@@ -2216,6 +2033,9 @@
           .monotonic_remote_time = {msg->monotonic_remote_boot,
                                     msg->data->monotonic_remote_time.value()},
           .realtime_remote_time = msg->data->realtime_remote_time.value(),
+          .monotonic_remote_transmit_time =
+              {msg->monotonic_remote_boot,
+               msg->data->monotonic_remote_transmit_time},
           .monotonic_timestamp_time = {msg->monotonic_timestamp_boot,
                                        msg->data->monotonic_timestamp_time},
           .data = std::move(msg->data)});
@@ -2381,6 +2201,9 @@
       source_node_.emplace_back(configuration::GetNodeIndex(
           config, channel->source_node()->string_view()));
     }
+  } else {
+    // The node index for single-node logs is always 0.
+    source_node_.resize(config->channels()->size(), 0);
   }
 }
 
@@ -2404,16 +2227,17 @@
 }
 
 void TimestampMapper::QueueMessage(const Message *msg) {
-  matched_messages_.emplace_back(
-      TimestampedMessage{.channel_index = msg->channel_index,
-                         .queue_index = msg->queue_index,
-                         .monotonic_event_time = msg->timestamp,
-                         .realtime_event_time = msg->data->realtime_sent_time,
-                         .remote_queue_index = BootQueueIndex::Invalid(),
-                         .monotonic_remote_time = BootTimestamp::min_time(),
-                         .realtime_remote_time = realtime_clock::min_time,
-                         .monotonic_timestamp_time = BootTimestamp::min_time(),
-                         .data = std::move(msg->data)});
+  matched_messages_.emplace_back(TimestampedMessage{
+      .channel_index = msg->channel_index,
+      .queue_index = msg->queue_index,
+      .monotonic_event_time = msg->timestamp,
+      .realtime_event_time = msg->data->realtime_sent_time,
+      .remote_queue_index = BootQueueIndex::Invalid(),
+      .monotonic_remote_time = BootTimestamp::min_time(),
+      .realtime_remote_time = realtime_clock::min_time,
+      .monotonic_remote_transmit_time = BootTimestamp::min_time(),
+      .monotonic_timestamp_time = BootTimestamp::min_time(),
+      .data = std::move(msg->data)});
   VLOG(1) << node_name() << " Inserted " << matched_messages_.back();
 }
 
@@ -2536,6 +2360,9 @@
         .monotonic_remote_time = {msg->monotonic_remote_boot,
                                   msg->data->monotonic_remote_time.value()},
         .realtime_remote_time = msg->data->realtime_remote_time.value(),
+        .monotonic_remote_transmit_time =
+            {msg->monotonic_remote_boot,
+             msg->data->monotonic_remote_transmit_time},
         .monotonic_timestamp_time = {msg->monotonic_timestamp_boot,
                                      msg->data->monotonic_timestamp_time},
         .data = std::move(data.data)});
diff --git a/aos/events/logging/logfile_utils.h b/aos/events/logging/logfile_utils.h
index 8a569ba..d781a20 100644
--- a/aos/events/logging/logfile_utils.h
+++ b/aos/events/logging/logfile_utils.h
@@ -37,10 +37,6 @@
   // The message originated on another node, but only the delivery times are
   // logged here.
   kLogDeliveryTimeOnly,
-  // The message originated on another node. Log it and the delivery times
-  // together.  The message_gateway is responsible for logging any messages
-  // which didn't get delivered.
-  kLogMessageAndDeliveryTime,
   // The message originated on the other node and should be logged on this node.
   kLogRemoteMessage
 };
@@ -445,6 +441,7 @@
       realtime_clock::time_point realtime_sent_time, uint32_t queue_index,
       std::optional<monotonic_clock::time_point> monotonic_remote_time,
       std::optional<realtime_clock::time_point> realtime_remote_time,
+      monotonic_clock::time_point monotonic_remote_transmit_time,
       std::optional<uint32_t> remote_queue_index,
       monotonic_clock::time_point monotonic_timestamp_time,
       bool has_monotonic_timestamp_time, absl::Span<const uint8_t> span)
@@ -454,6 +451,7 @@
         queue_index(queue_index),
         monotonic_remote_time(monotonic_remote_time),
         realtime_remote_time(realtime_remote_time),
+        monotonic_remote_transmit_time(monotonic_remote_transmit_time),
         remote_queue_index(remote_queue_index),
         monotonic_timestamp_time(monotonic_timestamp_time),
         has_monotonic_timestamp_time(has_monotonic_timestamp_time),
@@ -473,6 +471,7 @@
   std::optional<aos::monotonic_clock::time_point> monotonic_remote_time;
 
   std::optional<realtime_clock::time_point> realtime_remote_time;
+  aos::monotonic_clock::time_point monotonic_remote_transmit_time;
   std::optional<uint32_t> remote_queue_index;
 
   // This field is defaulted in the flatbuffer, so we need to store both the
@@ -550,6 +549,8 @@
   BootTimestamp monotonic_remote_time;
   realtime_clock::time_point realtime_remote_time = realtime_clock::min_time;
 
+  BootTimestamp monotonic_remote_transmit_time;
+
   BootTimestamp monotonic_timestamp_time;
 
   std::shared_ptr<UnpackedMessageHeader> data;
diff --git a/aos/events/logging/logfile_utils_test.cc b/aos/events/logging/logfile_utils_test.cc
index 30a0ada..4ceca20 100644
--- a/aos/events/logging/logfile_utils_test.cc
+++ b/aos/events/logging/logfile_utils_test.cc
@@ -326,11 +326,44 @@
   return fbb2.Release();
 }
 
+// Allows for some customization of a SortingElementTest.
+enum class SortingElementConfig {
+  // Create a single node configuration.
+  kSingleNode,
+  // Create a multi-node configuration.
+  kMultiNode,
+};
+
+template <SortingElementConfig sorting_element_config =
+              SortingElementConfig::kMultiNode>
 class SortingElementTest : public ::testing::Test {
  public:
   SortingElementTest()
       : config_(JsonToFlatbuffer<Configuration>(
-            R"({
+            sorting_element_config == SortingElementConfig::kSingleNode ?
+                                                                        R"({
+  "channels": [
+    {
+      "name": "/a",
+      "type": "aos.logger.testing.TestMessage"
+    },
+    {
+      "name": "/b",
+      "type": "aos.logger.testing.TestMessage"
+    },
+    {
+      "name": "/c",
+      "type": "aos.logger.testing.TestMessage"
+    },
+    {
+      "name": "/d",
+      "type": "aos.logger.testing.TestMessage"
+    }
+  ]
+}
+)"
+                                                                        :
+                                                                        R"({
   "channels": [
     {
       "name": "/a",
@@ -379,7 +412,31 @@
   ]
 }
 )")),
-        config0_(MakeHeader(config_, R"({
+        config0_(MakeHeader(
+            config_, sorting_element_config == SortingElementConfig::kSingleNode
+                         ?
+                         R"({
+  /* 100ms */
+  "max_out_of_order_duration": 100000000,
+  "node": {
+    "name": "pi1"
+  },
+  "logger_node": {
+    "name": "pi1"
+  },
+  "monotonic_start_time": 1000000,
+  "realtime_start_time": 1000000000000,
+  "log_event_uuid": "30ef1283-81d7-4004-8c36-1c162dbcb2b2",
+  "source_node_boot_uuid": "1d782c63-b3c7-466e-bea9-a01308b43333",
+  "logger_node_boot_uuid": "1d782c63-b3c7-466e-bea9-a01308b43333",
+  "boot_uuids": [
+    "1d782c63-b3c7-466e-bea9-a01308b43333",
+  ],
+  "parts_uuid": "2a05d725-5d5c-4c0b-af42-88de2f3c3876",
+  "parts_index": 0
+})"
+                         :
+                         R"({
   /* 100ms */
   "max_out_of_order_duration": 100000000,
   "node": {
@@ -593,10 +650,13 @@
   std::vector<uint32_t> queue_index_;
 };
 
-using MessageSorterTest = SortingElementTest;
+using MessageSorterTest = SortingElementTest<SortingElementConfig::kMultiNode>;
 using MessageSorterDeathTest = MessageSorterTest;
-using PartsMergerTest = SortingElementTest;
-using TimestampMapperTest = SortingElementTest;
+using PartsMergerTest = SortingElementTest<SortingElementConfig::kMultiNode>;
+using TimestampMapperTest =
+    SortingElementTest<SortingElementConfig::kMultiNode>;
+using SingleNodeTimestampMapperTest =
+    SortingElementTest<SortingElementConfig::kSingleNode>;
 
 // Tests that we can pull messages out of a log sorted in order.
 TEST_F(MessageSorterTest, Pull) {
@@ -2065,7 +2125,44 @@
   }
 }
 
-class BootMergerTest : public SortingElementTest {
+// Validates that we can read timestamps on startup even for single-node logs.
+TEST_F(SingleNodeTimestampMapperTest, QueueTimestampsForSingleNodes) {
+  const aos::monotonic_clock::time_point e = monotonic_clock::epoch();
+  {
+    TestDetachedBufferWriter writer0(logfile0_);
+    writer0.QueueSpan(config0_.span());
+
+    writer0.WriteSizedFlatbuffer(
+        MakeLogMessage(e + chrono::milliseconds(1000), 0, 0x005));
+    writer0.WriteSizedFlatbuffer(
+        MakeLogMessage(e + chrono::milliseconds(2000), 0, 0x006));
+    writer0.WriteSizedFlatbuffer(
+        MakeLogMessage(e + chrono::milliseconds(2000), 0, 0x007));
+    writer0.WriteSizedFlatbuffer(
+        MakeLogMessage(e + chrono::milliseconds(3000), 0, 0x008));
+  }
+
+  const std::vector<LogFile> parts = SortParts({logfile0_});
+  LogFilesContainer log_files(parts);
+
+  ASSERT_EQ(parts[0].logger_node, "pi1");
+
+  size_t mapper0_count = 0;
+  TimestampMapper mapper0("pi1", log_files,
+                          TimestampQueueStrategy::kQueueTimestampsAtStartup);
+  mapper0.set_timestamp_callback(
+      [&](TimestampedMessage *) { ++mapper0_count; });
+  mapper0.QueueTimestamps();
+
+  for (int i = 0; i < 4; ++i) {
+    ASSERT_TRUE(mapper0.Front() != nullptr);
+    mapper0.PopFront();
+  }
+  EXPECT_TRUE(mapper0.Front() == nullptr);
+  EXPECT_EQ(mapper0_count, 4u);
+}
+
+class BootMergerTest : public SortingElementTest<> {
  public:
   BootMergerTest()
       : SortingElementTest(),
@@ -2204,7 +2301,7 @@
   EXPECT_EQ(output[3].timestamp.time, e + chrono::milliseconds(200));
 }
 
-class RebootTimestampMapperTest : public SortingElementTest {
+class RebootTimestampMapperTest : public SortingElementTest<> {
  public:
   RebootTimestampMapperTest()
       : SortingElementTest(),
@@ -2730,7 +2827,7 @@
   }
 }
 
-class SortingDeathTest : public SortingElementTest {
+class SortingDeathTest : public SortingElementTest<> {
  public:
   SortingDeathTest()
       : SortingElementTest(),
@@ -3070,6 +3167,10 @@
         aos::realtime_clock::epoch() +
         chrono::nanoseconds(time_distribution(random_number_generator_));
 
+    context.monotonic_remote_transmit_time =
+        aos::monotonic_clock::epoch() +
+        chrono::nanoseconds(time_distribution(random_number_generator_));
+
     context.queue_index = uint32_distribution(random_number_generator_);
     context.remote_queue_index = uint32_distribution(random_number_generator_);
     context.size = data_.size();
@@ -3109,6 +3210,9 @@
     builder.add_remote_queue_index(
         uint8_distribution(random_number_generator_));
 
+    builder.add_monotonic_remote_transmit_time(
+        time_distribution(random_number_generator_));
+
     fbb.FinishSizePrefixed(builder.Finish());
     return fbb.Release();
   }
@@ -3254,7 +3358,7 @@
 
   for (const LogType type :
        {LogType::kLogMessage, LogType::kLogDeliveryTimeOnly,
-        LogType::kLogMessageAndDeliveryTime, LogType::kLogRemoteMessage}) {
+        LogType::kLogRemoteMessage}) {
     for (int i = 0; i < 100; ++i) {
       aos::Context context = RandomContext();
       const uint32_t channel_index =
@@ -3282,11 +3386,18 @@
           repacked_message.size(),
           PackMessageInline(repacked_message.data(), context, channel_index,
                             type, 0u, repacked_message.size()));
-      EXPECT_EQ(absl::Span<uint8_t>(repacked_message),
+      for (size_t i = 0; i < fbb.GetBufferSpan().size(); ++i) {
+        ASSERT_EQ(absl::Span<uint8_t>(repacked_message)[i],
+                  absl::Span<uint8_t>(fbb.GetBufferSpan().data(),
+                                      fbb.GetBufferSpan().size())[i])
+            << ": On index " << i;
+      }
+      ASSERT_EQ(absl::Span<uint8_t>(repacked_message),
                 absl::Span<uint8_t>(fbb.GetBufferSpan().data(),
                                     fbb.GetBufferSpan().size()))
           << AnnotateBinaries(schema, "aos/events/logging/logger.bfbs",
-                              fbb.GetBufferSpan());
+                              fbb.GetBufferSpan())
+          << " for log type " << static_cast<int>(type);
 
       // Ok, now we want to confirm that we can build up arbitrary pieces of
       // said flatbuffer.  Try all of them since it is cheap.
diff --git a/aos/events/logging/logger.fbs b/aos/events/logging/logger.fbs
index 5733fa0..f24922a 100644
--- a/aos/events/logging/logger.fbs
+++ b/aos/events/logging/logger.fbs
@@ -206,6 +206,12 @@
   oldest_remote_reliable_monotonic_timestamps:[int64] (id: 28);
   oldest_local_reliable_monotonic_timestamps:[int64] (id: 29);
 
+  // For all channels *excluding* the unreliable channels (ttl != 0), record the
+  // oldest time a reliable message was sent to the kernel to be transmitted over
+  // the network.
+  oldest_remote_reliable_monotonic_transmit_timestamps:[int64] (id: 35);
+  oldest_local_reliable_monotonic_transmit_timestamps:[int64] (id: 36);
+
   // For all the remote timestamps which come back to the logger.  The "local"
   // time here is the logger node boot, and "remote" is the node which sent the
   // timestamps.
@@ -257,6 +263,30 @@
   // Time this timestamp was received on the monotonic clock of the logger node
   // in nanoseconds.
   monotonic_timestamp_time:int64 = -9223372036854775808 (id: 8);
+
+  // Time that this message was handed to the kernel to be published over the
+  // network on the remote node.
+  //
+  // monotonic_sent_time captures the time the message was pushed into queue.
+  // For reliable messages (ttl != 0) this may not be equal to the time it was
+  // published.
+  //
+  // For example:
+  // 1. Node A sends a reliable message RM1 to the queue at 10 seconds.
+  // 2. Node B reboots at 80 seconds and requests for all reliable messages.
+  // 3. The reliable message RM1 is sent to node B with a sent time of 10 seconds
+  //    even though time is past 80 seconds.
+  // 4. Using this as a valid timestamp point confuses the timestamp solver.
+  //
+  // i.e. monotonic_sent_time for reliable messages is not very useful to
+  // estimate network latency.
+  //
+  // Having more timestamp information such as transmit time can improve reliablity
+  // of the log reader (timestamp solver) especially when reliable messages are involved.
+  //
+  // This data also provides higher visibility into the path of a message across the
+  // network.
+  monotonic_remote_transmit_time:int64 = -9223372036854775808 (id: 9);
 }
 
 root_type MessageHeader;
diff --git a/aos/events/logging/logger_test.cc b/aos/events/logging/logger_test.cc
index 3f0b182..7799c57 100644
--- a/aos/events/logging/logger_test.cc
+++ b/aos/events/logging/logger_test.cc
@@ -77,11 +77,10 @@
   LOG(INFO) << "Logging data to " << logfile;
 
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_separate_config(false);
     logger.set_polling_period(std::chrono::milliseconds(100));
@@ -141,11 +140,10 @@
   LOG(INFO) << "Logging data to " << logfile;
 
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_separate_config(false);
     logger.set_polling_period(std::chrono::milliseconds(100));
@@ -202,11 +200,10 @@
   LOG(INFO) << "Logging data to " << logfile1 << " then " << logfile2;
 
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_polling_period(std::chrono::milliseconds(100));
     logger_event_loop->OnRun([base_name1, base_name2, &logger_event_loop,
@@ -277,11 +274,10 @@
   LOG(INFO) << "Logging data to " << logfile;
 
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_separate_config(false);
     logger.set_polling_period(std::chrono::milliseconds(100));
@@ -316,11 +312,10 @@
   LOG(INFO) << "Logging data to " << logfile1 << " then " << logfile2;
 
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_separate_config(false);
     logger.set_polling_period(std::chrono::milliseconds(100));
@@ -385,11 +380,10 @@
   LOG(INFO) << "Logging data to " << logfile0 << " and " << logfile1;
 
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_separate_config(false);
     logger.set_polling_period(std::chrono::milliseconds(100));
diff --git a/aos/events/logging/multinode_logger_test.cc b/aos/events/logging/multinode_logger_test.cc
index 3c438e3..2cf8ffe 100644
--- a/aos/events/logging/multinode_logger_test.cc
+++ b/aos/events/logging/multinode_logger_test.cc
@@ -79,6 +79,44 @@
                          ForceTimestampBuffering::kAutoBuffer}),
         ::testing::ValuesIn(SupportedCompressionAlgorithms())));
 
+// Class to spam Pong messages blindly.
+class PongSender {
+ public:
+  PongSender(EventLoop *loop, std::string_view channel_name)
+      : sender_(loop->MakeSender<examples::Pong>(channel_name)) {
+    loop->AddPhasedLoop(
+        [this](int) {
+          aos::Sender<examples::Pong>::Builder builder = sender_.MakeBuilder();
+          examples::Pong::Builder pong_builder =
+              builder.MakeBuilder<examples::Pong>();
+          CHECK_EQ(builder.Send(pong_builder.Finish()), RawSender::Error::kOk);
+        },
+        chrono::milliseconds(10));
+  }
+
+ private:
+  aos::Sender<examples::Pong> sender_;
+};
+
+// Class to spam Ping messages blindly.
+class PingSender {
+ public:
+  PingSender(EventLoop *loop, std::string_view channel_name)
+      : sender_(loop->MakeSender<examples::Ping>(channel_name)) {
+    loop->AddPhasedLoop(
+        [this](int) {
+          aos::Sender<examples::Ping>::Builder builder = sender_.MakeBuilder();
+          examples::Ping::Builder ping_builder =
+              builder.MakeBuilder<examples::Ping>();
+          CHECK_EQ(builder.Send(ping_builder.Finish()), RawSender::Error::kOk);
+        },
+        chrono::milliseconds(10));
+  }
+
+ private:
+  aos::Sender<examples::Ping> sender_;
+};
+
 // Tests that we can write and read simple multi-node log files.
 TEST_P(MultinodeLoggerTest, SimpleMultiNode) {
   if (file_strategy() == FileStrategy::kCombine) {
@@ -436,6 +474,8 @@
                   pi1_event_loop->context().monotonic_event_time);
         EXPECT_EQ(pi1_event_loop->context().realtime_remote_time,
                   pi1_event_loop->context().realtime_event_time);
+        EXPECT_EQ(pi1_event_loop->context().monotonic_remote_transmit_time,
+                  monotonic_clock::min_time);
 
         ++pi1_ping_count;
       });
@@ -452,11 +492,21 @@
         EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
                   pi2_ping_count * chrono::milliseconds(10) +
                       realtime_clock::epoch());
-        EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time +
-                      chrono::microseconds(150),
+        // The message at the start of each second doesn't have wakeup latency
+        // since timing reports and server statistics wake us up already at that
+        // point in time.
+        chrono::nanoseconds offset = chrono::microseconds(150);
+        if (pi2_event_loop->context().monotonic_remote_time.time_since_epoch() %
+                chrono::seconds(1) ==
+            chrono::seconds(0)) {
+          offset = chrono::microseconds(100);
+        }
+        EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time + offset,
                   pi2_event_loop->context().monotonic_event_time);
-        EXPECT_EQ(pi2_event_loop->context().realtime_remote_time +
-                      chrono::microseconds(150),
+        EXPECT_EQ(pi2_event_loop->context().monotonic_event_time -
+                      chrono::microseconds(100),
+                  pi2_event_loop->context().monotonic_remote_transmit_time);
+        EXPECT_EQ(pi2_event_loop->context().realtime_remote_time + offset,
                   pi2_event_loop->context().realtime_event_time);
         ++pi2_ping_count;
       });
@@ -464,63 +514,81 @@
   constexpr ssize_t kQueueIndexOffset = -9;
   // Confirm that the ping and pong counts both match, and the value also
   // matches.
-  pi1_event_loop->MakeWatcher(
-      "/test", [&pi1_event_loop, &pi1_ping_count,
-                &pi1_pong_count](const examples::Pong &pong) {
-        VLOG(1) << "Pi1 pong " << FlatbufferToJson(&pong) << " at "
-                << pi1_event_loop->context().monotonic_remote_time << " -> "
-                << pi1_event_loop->context().monotonic_event_time;
+  pi1_event_loop->MakeWatcher("/test", [&pi1_event_loop, &pi1_ping_count,
+                                        &pi1_pong_count](
+                                           const examples::Pong &pong) {
+    VLOG(1) << "Pi1 pong " << FlatbufferToJson(&pong) << " at "
+            << pi1_event_loop->context().monotonic_remote_time << " -> "
+            << pi1_event_loop->context().monotonic_event_time;
 
-        EXPECT_EQ(pi1_event_loop->context().remote_queue_index,
-                  pi1_pong_count + kQueueIndexOffset);
-        EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time,
-                  chrono::microseconds(200) +
-                      pi1_pong_count * chrono::milliseconds(10) +
-                      monotonic_clock::epoch());
-        EXPECT_EQ(pi1_event_loop->context().realtime_remote_time,
-                  chrono::microseconds(200) +
-                      pi1_pong_count * chrono::milliseconds(10) +
-                      realtime_clock::epoch());
+    EXPECT_EQ(pi1_event_loop->context().remote_queue_index,
+              pi1_pong_count + kQueueIndexOffset);
 
-        EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time +
-                      chrono::microseconds(150),
-                  pi1_event_loop->context().monotonic_event_time);
-        EXPECT_EQ(pi1_event_loop->context().realtime_remote_time +
-                      chrono::microseconds(150),
-                  pi1_event_loop->context().realtime_event_time);
+    chrono::nanoseconds offset = chrono::microseconds(200);
+    if ((pi1_event_loop->context().monotonic_remote_time.time_since_epoch() -
+         chrono::microseconds(150)) %
+            chrono::seconds(1) ==
+        chrono::seconds(0)) {
+      offset = chrono::microseconds(150);
+    }
 
-        EXPECT_EQ(pong.value(), pi1_pong_count + 1);
-        ++pi1_pong_count;
-        EXPECT_EQ(pi1_ping_count, pi1_pong_count);
-      });
-  pi2_event_loop->MakeWatcher(
-      "/test", [&pi2_event_loop, &pi2_ping_count,
-                &pi2_pong_count](const examples::Pong &pong) {
-        VLOG(1) << "Pi2 pong " << FlatbufferToJson(&pong) << " at "
-                << pi2_event_loop->context().monotonic_remote_time << " -> "
-                << pi2_event_loop->context().monotonic_event_time;
+    EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time,
+              offset + pi1_pong_count * chrono::milliseconds(10) +
+                  monotonic_clock::epoch());
+    EXPECT_EQ(pi1_event_loop->context().realtime_remote_time,
+              offset + pi1_pong_count * chrono::milliseconds(10) +
+                  realtime_clock::epoch());
 
-        EXPECT_EQ(pi2_event_loop->context().remote_queue_index,
-                  pi2_pong_count + kQueueIndexOffset);
+    EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time +
+                  chrono::microseconds(150),
+              pi1_event_loop->context().monotonic_event_time);
+    EXPECT_EQ(pi1_event_loop->context().realtime_remote_time +
+                  chrono::microseconds(150),
+              pi1_event_loop->context().realtime_event_time);
+    EXPECT_EQ(pi1_event_loop->context().monotonic_remote_transmit_time,
+              pi1_event_loop->context().monotonic_event_time -
+                  chrono::microseconds(100));
 
-        EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time,
-                  chrono::microseconds(200) +
-                      pi2_pong_count * chrono::milliseconds(10) +
-                      monotonic_clock::epoch());
-        EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
-                  chrono::microseconds(200) +
-                      pi2_pong_count * chrono::milliseconds(10) +
-                      realtime_clock::epoch());
+    EXPECT_EQ(pong.value(), pi1_pong_count + 1);
+    ++pi1_pong_count;
+    EXPECT_EQ(pi1_ping_count, pi1_pong_count);
+  });
+  pi2_event_loop->MakeWatcher("/test", [&pi2_event_loop, &pi2_ping_count,
+                                        &pi2_pong_count](
+                                           const examples::Pong &pong) {
+    VLOG(1) << "Pi2 pong " << FlatbufferToJson(&pong) << " at "
+            << pi2_event_loop->context().monotonic_remote_time << " -> "
+            << pi2_event_loop->context().monotonic_event_time;
 
-        EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time,
-                  pi2_event_loop->context().monotonic_event_time);
-        EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
-                  pi2_event_loop->context().realtime_event_time);
+    EXPECT_EQ(pi2_event_loop->context().remote_queue_index,
+              pi2_pong_count + kQueueIndexOffset);
 
-        EXPECT_EQ(pong.value(), pi2_pong_count + 1);
-        ++pi2_pong_count;
-        EXPECT_EQ(pi2_ping_count, pi2_pong_count);
-      });
+    chrono::nanoseconds offset = chrono::microseconds(200);
+    if ((pi2_event_loop->context().monotonic_remote_time.time_since_epoch() -
+         chrono::microseconds(150)) %
+            chrono::seconds(1) ==
+        chrono::seconds(0)) {
+      offset = chrono::microseconds(150);
+    }
+
+    EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time,
+              offset + pi2_pong_count * chrono::milliseconds(10) +
+                  monotonic_clock::epoch());
+    EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
+              offset + pi2_pong_count * chrono::milliseconds(10) +
+                  realtime_clock::epoch());
+
+    EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time,
+              pi2_event_loop->context().monotonic_event_time);
+    EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
+              pi2_event_loop->context().realtime_event_time);
+    EXPECT_EQ(pi2_event_loop->context().monotonic_remote_transmit_time,
+              monotonic_clock::min_time);
+
+    EXPECT_EQ(pong.value(), pi2_pong_count + 1);
+    ++pi2_pong_count;
+    EXPECT_EQ(pi2_ping_count, pi2_pong_count);
+  });
 
   log_reader_factory.Run();
   EXPECT_EQ(pi1_ping_count, 2010);
@@ -1855,6 +1923,9 @@
               chrono::nanoseconds(header.realtime_sent_time()));
           const aos::monotonic_clock::time_point header_monotonic_remote_time(
               chrono::nanoseconds(header.monotonic_remote_time()));
+          const aos::monotonic_clock::time_point
+              header_monotonic_remote_transmit_time(
+                  chrono::nanoseconds(header.monotonic_remote_transmit_time()));
           const aos::realtime_clock::time_point header_realtime_remote_time(
               chrono::nanoseconds(header.realtime_remote_time()));
 
@@ -1870,11 +1941,28 @@
             ASSERT_TRUE(pi1_timestamp_on_pi2_fetcher.FetchNext());
             pi1_context = &pi1_timestamp_on_pi1_fetcher.context();
             pi2_context = &pi1_timestamp_on_pi2_fetcher.context();
+            // Timestamps don't have wakeup delay, so they show back up after 2
+            // times the network delay on the source node.  Confirm that matches
+            // when we are reading the log.
+            EXPECT_EQ(pi1_event_loop->context().monotonic_event_time,
+                      pi1_context->monotonic_event_time + 2 * network_delay);
           } else if (header.channel_index() == ping_timestamp_channel) {
             ASSERT_TRUE(ping_on_pi1_fetcher.FetchNext());
             ASSERT_TRUE(ping_on_pi2_fetcher.FetchNext());
             pi1_context = &ping_on_pi1_fetcher.context();
             pi2_context = &ping_on_pi2_fetcher.context();
+            // Ping messages get picked up faster at the start of each message
+            // when timers wake up.  Verify all that behavior matches exactly as
+            // expected when reading the log.
+            EXPECT_EQ(pi1_event_loop->context().monotonic_event_time,
+                      pi1_context->monotonic_event_time + 2 * network_delay +
+                          ((pi1_event_loop->context().monotonic_event_time -
+                            2 * network_delay)
+                                           .time_since_epoch() %
+                                       chrono::nanoseconds(1000000000) ==
+                                   chrono::nanoseconds(0)
+                               ? chrono::nanoseconds(0)
+                               : send_delay));
           } else {
             LOG(FATAL) << "Unknown channel " << FlatbufferToJson(&header) << " "
                        << configuration::CleanedChannelToString(
@@ -1899,18 +1987,13 @@
                     header_realtime_remote_time);
           EXPECT_EQ(pi2_context->monotonic_remote_time,
                     header_monotonic_remote_time);
+          EXPECT_EQ(pi2_context->monotonic_remote_transmit_time,
+                    header_monotonic_remote_transmit_time);
 
           EXPECT_EQ(pi1_context->realtime_event_time,
                     header_realtime_remote_time);
           EXPECT_EQ(pi1_context->monotonic_event_time,
                     header_monotonic_remote_time);
-
-          // Time estimation isn't perfect, but we know the clocks were
-          // identical when logged, so we know when this should have come back.
-          // Confirm we got it when we expected.
-          EXPECT_EQ(pi1_event_loop->context().monotonic_event_time,
-                    pi1_context->monotonic_event_time + 2 * network_delay +
-                        send_delay);
         });
   }
   for (std::pair<int, std::string> channel :
@@ -1935,6 +2018,9 @@
               chrono::nanoseconds(header.realtime_sent_time()));
           const aos::monotonic_clock::time_point header_monotonic_remote_time(
               chrono::nanoseconds(header.monotonic_remote_time()));
+          const aos::monotonic_clock::time_point
+              header_monotonic_remote_transmit_time(
+                  chrono::nanoseconds(header.monotonic_remote_transmit_time()));
           const aos::realtime_clock::time_point header_realtime_remote_time(
               chrono::nanoseconds(header.realtime_remote_time()));
 
@@ -1950,11 +2036,20 @@
             ASSERT_TRUE(pi2_timestamp_on_pi1_fetcher.FetchNext());
             pi2_context = &pi2_timestamp_on_pi2_fetcher.context();
             pi1_context = &pi2_timestamp_on_pi1_fetcher.context();
+            // Again, timestamps don't have wakeup delay, so they show back up
+            // after 2 times the network delay on the source node.
+            EXPECT_EQ(pi2_event_loop->context().monotonic_event_time,
+                      pi2_context->monotonic_event_time + 2 * network_delay);
           } else if (header.channel_index() == pong_timestamp_channel) {
             ASSERT_TRUE(pong_on_pi2_fetcher.FetchNext());
             ASSERT_TRUE(pong_on_pi1_fetcher.FetchNext());
             pi2_context = &pong_on_pi2_fetcher.context();
             pi1_context = &pong_on_pi1_fetcher.context();
+            // And Pong messages come back repeatably since they aren't at the
+            // start of a second.
+            EXPECT_EQ(pi2_event_loop->context().monotonic_event_time,
+                      pi2_context->monotonic_event_time + 2 * network_delay +
+                          send_delay);
           } else {
             LOG(FATAL) << "Unknown channel " << FlatbufferToJson(&header) << " "
                        << configuration::CleanedChannelToString(
@@ -1979,18 +2074,13 @@
                     header_realtime_remote_time);
           EXPECT_EQ(pi1_context->monotonic_remote_time,
                     header_monotonic_remote_time);
+          EXPECT_EQ(pi1_context->monotonic_remote_transmit_time,
+                    header_monotonic_remote_transmit_time);
 
           EXPECT_EQ(pi2_context->realtime_event_time,
                     header_realtime_remote_time);
           EXPECT_EQ(pi2_context->monotonic_event_time,
                     header_monotonic_remote_time);
-
-          // Time estimation isn't perfect, but we know the clocks were
-          // identical when logged, so we know when this should have come back.
-          // Confirm we got it when we expected.
-          EXPECT_EQ(pi2_event_loop->context().monotonic_event_time,
-                    pi2_context->monotonic_event_time + 2 * network_delay +
-                        send_delay);
         });
   }
 
@@ -2228,7 +2318,7 @@
           case 3:
             EXPECT_EQ(source_node_boot_uuid, pi2_boot1);
             ASSERT_EQ(monotonic_start_time, monotonic_clock::epoch() +
-                                                chrono::nanoseconds(2322999462))
+                                                chrono::nanoseconds(2323000000))
                 << " on " << file;
             break;
           default:
@@ -2255,7 +2345,7 @@
           case 5:
             EXPECT_EQ(source_node_boot_uuid, pi2_boot1);
             ASSERT_EQ(monotonic_start_time, monotonic_clock::epoch() +
-                                                chrono::nanoseconds(2322999462))
+                                                chrono::nanoseconds(2323000000))
                 << " on " << file;
             break;
           default:
@@ -2320,6 +2410,18 @@
                     .oldest_local_unreliable_monotonic_timestamps()
                     ->Get(1)));
     const monotonic_clock::time_point
+        oldest_remote_reliable_monotonic_transmit_timestamps =
+            monotonic_clock::time_point(chrono::nanoseconds(
+                log_header->message()
+                    .oldest_remote_reliable_monotonic_transmit_timestamps()
+                    ->Get(1)));
+    const monotonic_clock::time_point
+        oldest_local_reliable_monotonic_transmit_timestamps =
+            monotonic_clock::time_point(chrono::nanoseconds(
+                log_header->message()
+                    .oldest_local_reliable_monotonic_transmit_timestamps()
+                    ->Get(1)));
+    const monotonic_clock::time_point
         oldest_remote_reliable_monotonic_timestamps =
             monotonic_clock::time_point(chrono::nanoseconds(
                 log_header->message()
@@ -2362,6 +2464,10 @@
                     monotonic_clock::max_time);
           EXPECT_EQ(oldest_local_reliable_monotonic_timestamps,
                     monotonic_clock::max_time);
+          EXPECT_EQ(oldest_remote_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::max_time);
+          EXPECT_EQ(oldest_local_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::max_time);
           break;
         default:
           FAIL();
@@ -2383,6 +2489,10 @@
                     monotonic_clock::max_time);
           EXPECT_EQ(oldest_local_reliable_monotonic_timestamps,
                     monotonic_clock::max_time);
+          EXPECT_EQ(oldest_remote_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::microseconds(90250)));
+          EXPECT_EQ(oldest_local_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::microseconds(90350)));
           break;
         case 1:
           ASSERT_EQ(oldest_remote_monotonic_timestamps,
@@ -2401,7 +2511,13 @@
                     monotonic_clock::time_point(chrono::microseconds(100000)))
               << file;
           EXPECT_EQ(oldest_local_reliable_monotonic_timestamps,
-                    monotonic_clock::time_point(chrono::microseconds(100150)))
+                    monotonic_clock::time_point(chrono::microseconds(100100)))
+              << file;
+          EXPECT_EQ(oldest_remote_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::microseconds(90250)))
+              << file;
+          EXPECT_EQ(oldest_local_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::microseconds(90350)))
               << file;
           break;
         case 2:
@@ -2423,6 +2539,13 @@
           EXPECT_EQ(oldest_local_reliable_monotonic_timestamps,
                     monotonic_clock::max_time)
               << file;
+          EXPECT_EQ(oldest_remote_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::milliseconds(1323) +
+                                                chrono::microseconds(250)))
+              << file;
+          EXPECT_EQ(oldest_local_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::microseconds(10100350)))
+              << file;
           break;
         case 3:
           ASSERT_EQ(oldest_remote_monotonic_timestamps,
@@ -2441,7 +2564,14 @@
                     monotonic_clock::time_point(chrono::microseconds(1423000)))
               << file;
           EXPECT_EQ(oldest_local_reliable_monotonic_timestamps,
-                    monotonic_clock::time_point(chrono::microseconds(10200150)))
+                    monotonic_clock::time_point(chrono::microseconds(10200100)))
+              << file;
+          EXPECT_EQ(oldest_remote_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::milliseconds(1323) +
+                                                chrono::microseconds(250)))
+              << file;
+          EXPECT_EQ(oldest_local_reliable_monotonic_transmit_timestamps,
+                    monotonic_clock::time_point(chrono::microseconds(10100350)))
               << file;
           break;
         default:
@@ -3124,6 +3254,107 @@
   }
 }
 
+// Tests that we can relog with a subset of the original config. This is useful
+// for excluding obsolete or deprecated channels, so they don't appear in the
+// configuration when reading the log.
+TEST_P(MultinodeLoggerTest, LogPartialConfig) {
+  time_converter_.StartEqual();
+  {
+    LoggerState pi1_logger = MakeLogger(pi1_);
+    LoggerState pi2_logger = MakeLogger(pi2_);
+
+    event_loop_factory_.RunFor(chrono::milliseconds(95));
+
+    StartLogger(&pi1_logger);
+    StartLogger(&pi2_logger);
+
+    event_loop_factory_.RunFor(chrono::milliseconds(20000));
+  }
+
+  auto sorted_parts = SortParts(logfiles_);
+  EXPECT_TRUE(AllPartsMatchOutOfOrderDuration(sorted_parts));
+  LogReader reader(sorted_parts);
+  reader.RemapLoggedChannel<aos::examples::Ping>("/test", "/original");
+
+  SimulatedEventLoopFactory log_reader_factory(reader.configuration());
+  log_reader_factory.set_send_delay(chrono::microseconds(0));
+
+  // This sends out the fetched messages and advances time to the start of the
+  // log file.
+  reader.Register(&log_reader_factory);
+
+  const Node *pi1 =
+      configuration::GetNode(log_reader_factory.configuration(), "pi1");
+  const Node *pi2 =
+      configuration::GetNode(log_reader_factory.configuration(), "pi2");
+
+  LOG(INFO) << "Start time " << reader.monotonic_start_time(pi1) << " pi1";
+  LOG(INFO) << "Start time " << reader.monotonic_start_time(pi2) << " pi2";
+  LOG(INFO) << "now pi1 "
+            << log_reader_factory.GetNodeEventLoopFactory(pi1)->monotonic_now();
+  LOG(INFO) << "now pi2 "
+            << log_reader_factory.GetNodeEventLoopFactory(pi2)->monotonic_now();
+
+  EXPECT_THAT(reader.LoggedNodes(),
+              ::testing::ElementsAre(
+                  configuration::GetNode(reader.logged_configuration(), pi1),
+                  configuration::GetNode(reader.logged_configuration(), pi2)));
+
+  reader.event_loop_factory()->set_send_delay(chrono::microseconds(0));
+
+  const FlatbufferDetachedBuffer<Configuration> partial_configuration_buffer =
+      configuration::GetPartialConfiguration(
+          *reader.event_loop_factory()->configuration(),
+          [](const Channel &channel) {
+            if (channel.name()->string_view().starts_with("/original/")) {
+              LOG(INFO) << "Omitting channel from save_log, channel: "
+                        << channel.name()->string_view() << ", "
+                        << channel.type()->string_view();
+              return false;
+            }
+            return true;
+          });
+
+  // And confirm we can re-create a log again, while checking the contents.
+  std::vector<std::string> log_files;
+  {
+    const Configuration *partial_configuration =
+        &(partial_configuration_buffer.message());
+
+    LoggerState pi1_logger =
+        MakeLogger(log_reader_factory.GetNodeEventLoopFactory("pi1"),
+                   &log_reader_factory, partial_configuration);
+    LoggerState pi2_logger =
+        MakeLogger(log_reader_factory.GetNodeEventLoopFactory("pi2"),
+                   &log_reader_factory, partial_configuration);
+
+    pi1_logger.StartLogger(tmp_dir_ + "/logs/relogged1");
+    pi2_logger.StartLogger(tmp_dir_ + "/logs/relogged2");
+
+    log_reader_factory.Run();
+
+    for (auto &x : pi1_logger.log_namer->all_filenames()) {
+      log_files.emplace_back(absl::StrCat(tmp_dir_, "/logs/relogged1_", x));
+    }
+    for (auto &x : pi2_logger.log_namer->all_filenames()) {
+      log_files.emplace_back(absl::StrCat(tmp_dir_, "/logs/relogged2_", x));
+    }
+  }
+
+  reader.Deregister();
+
+  // And verify that we can run the LogReader over the relogged files without
+  // hitting any fatal errors.
+  {
+    auto sorted_parts = SortParts(log_files);
+    EXPECT_TRUE(AllPartsMatchOutOfOrderDuration(sorted_parts));
+    LogReader relogged_reader(sorted_parts);
+    relogged_reader.Register();
+
+    relogged_reader.event_loop_factory()->Run();
+  }
+}
+
 // Tests that we properly replay a log where the start time for a node is
 // before any data on the node.  This can happen if the logger starts before
 // data is published.  While the scenario below is a bit convoluted, we have
@@ -3288,7 +3519,7 @@
                                                       chrono::seconds(1)));
   EXPECT_THAT(result[2].second,
               ::testing::ElementsAre(realtime_clock::epoch() +
-                                     chrono::microseconds(34900150)));
+                                     chrono::microseconds(34900100)));
 }
 
 // Tests that local data before remote data after reboot is properly replayed.
@@ -3440,7 +3671,7 @@
   EXPECT_THAT(result[0].first, ::testing::ElementsAre(realtime_clock::epoch()));
   EXPECT_THAT(result[0].second,
               ::testing::ElementsAre(realtime_clock::epoch() +
-                                     chrono::microseconds(11000350)));
+                                     chrono::microseconds(11000300)));
 
   EXPECT_THAT(result[1].first,
               ::testing::ElementsAre(
@@ -3448,13 +3679,13 @@
                   realtime_clock::epoch() + chrono::microseconds(107005000)));
   EXPECT_THAT(result[1].second,
               ::testing::ElementsAre(
-                  realtime_clock::epoch() + chrono::microseconds(4000150),
-                  realtime_clock::epoch() + chrono::microseconds(111000200)));
+                  realtime_clock::epoch() + chrono::microseconds(4000100),
+                  realtime_clock::epoch() + chrono::microseconds(111000150)));
 
   EXPECT_THAT(result[2].first, ::testing::ElementsAre(realtime_clock::epoch()));
   EXPECT_THAT(result[2].second,
               ::testing::ElementsAre(realtime_clock::epoch() +
-                                     chrono::microseconds(11000150)));
+                                     chrono::microseconds(11000100)));
 
   auto start_stop_result = ConfirmReadable(
       filenames, realtime_clock::epoch() + chrono::milliseconds(2000),
@@ -3598,7 +3829,7 @@
   EXPECT_THAT(result[0].first, ::testing::ElementsAre(realtime_clock::epoch()));
   EXPECT_THAT(result[0].second,
               ::testing::ElementsAre(realtime_clock::epoch() +
-                                     chrono::microseconds(11000350)));
+                                     chrono::microseconds(11000300)));
 
   EXPECT_THAT(result[1].first,
               ::testing::ElementsAre(
@@ -3606,13 +3837,13 @@
                   realtime_clock::epoch() + chrono::microseconds(6005000)));
   EXPECT_THAT(result[1].second,
               ::testing::ElementsAre(
-                  realtime_clock::epoch() + chrono::microseconds(4900150),
-                  realtime_clock::epoch() + chrono::microseconds(11000200)));
+                  realtime_clock::epoch() + chrono::microseconds(4900100),
+                  realtime_clock::epoch() + chrono::microseconds(11000150)));
 
   EXPECT_THAT(result[2].first, ::testing::ElementsAre(realtime_clock::epoch()));
   EXPECT_THAT(result[2].second,
               ::testing::ElementsAre(realtime_clock::epoch() +
-                                     chrono::microseconds(11000150)));
+                                     chrono::microseconds(11000100)));
 
   // Confirm we observed the correct start and stop times.  We should see the
   // reboot here.
@@ -3632,7 +3863,7 @@
                   realtime_clock::epoch() + chrono::microseconds(6005000)));
   EXPECT_THAT(start_stop_result[1].second,
               ::testing::ElementsAre(
-                  realtime_clock::epoch() + chrono::microseconds(4900150),
+                  realtime_clock::epoch() + chrono::microseconds(4900100),
                   realtime_clock::epoch() + chrono::seconds(8)));
   EXPECT_THAT(
       start_stop_result[2].first,
@@ -4585,24 +4816,71 @@
   auto result = ConfirmReadable(filenames);
 }
 
-// Class to spam Pong messages blindly.
-class PongSender {
- public:
-  PongSender(EventLoop *loop, std::string_view channel_name)
-      : sender_(loop->MakeSender<examples::Pong>(channel_name)) {
-    loop->AddPhasedLoop(
-        [this](int) {
-          aos::Sender<examples::Pong>::Builder builder = sender_.MakeBuilder();
-          examples::Pong::Builder pong_builder =
-              builder.MakeBuilder<examples::Pong>();
-          CHECK_EQ(builder.Send(pong_builder.Finish()), RawSender::Error::kOk);
-        },
-        chrono::milliseconds(10));
+// Tests that only having a delayed, reliable message from a boot results in a
+// readable log.
+//
+// Note: this is disabled since it doesn't work yet.  Un-disable this when the
+// code is fixed!
+TEST(MultinodeLoggerLoopTest, ReliableOnlyTimestamps) {
+  util::UnlinkRecursive(aos::testing::TestTmpDir() + "/logs");
+  std::filesystem::create_directory(aos::testing::TestTmpDir() + "/logs");
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(
+          ArtifactPath("aos/events/logging/"
+                       "multinode_pingpong_reboot_reliable_only_config.json"));
+  message_bridge::TestingTimeConverter time_converter(
+      configuration::NodesCount(&config.message()));
+  SimulatedEventLoopFactory event_loop_factory(&config.message());
+  event_loop_factory.SetTimeConverter(&time_converter);
+
+  constexpr chrono::nanoseconds kRebootTime = chrono::seconds(100);
+  {
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch(),
+        {BootTimestamp::epoch(), BootTimestamp::epoch()});
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch() + kRebootTime,
+        {BootTimestamp::epoch() + kRebootTime,
+         BootTimestamp{.boot = 1, .time = monotonic_clock::epoch()}});
   }
 
- private:
-  aos::Sender<examples::Pong> sender_;
-};
+  const std::string kLogfile1_1 =
+      aos::testing::TestTmpDir() + "/logs/multi_logfile1/";
+
+  NodeEventLoopFactory *const pi1 =
+      event_loop_factory.GetNodeEventLoopFactory("pi1");
+
+  // We want unreliable timestamps from one boot, a reliable timestamp from the
+  // same boot, and then a long delayed reliable timestamp from the second boot.
+  // This produces conflicting information about when the second boot happened.
+  std::vector<std::string> filenames;
+  PingSender *app1 = pi1->AlwaysStart<PingSender>("pingsender", "/atest1");
+  PingSender *app2 = pi1->AlwaysStart<PingSender>("pingsender", "/atest2");
+  event_loop_factory.RunFor(chrono::seconds(1));
+  pi1->Stop(app2);
+  event_loop_factory.RunFor(kRebootTime - chrono::seconds(2));
+  pi1->Stop(app1);
+
+  event_loop_factory.RunFor(chrono::seconds(1) + kRebootTime * 2);
+
+  {
+    // Collect a small log after reboot.
+    LoggerState pi1_logger = MakeLoggerState(
+        pi1, &event_loop_factory, SupportedCompressionAlgorithms()[0],
+        FileStrategy::kKeepSeparate);
+    pi1_logger.StartLogger(kLogfile1_1);
+
+    event_loop_factory.RunFor(chrono::seconds(1));
+
+    pi1_logger.AppendAllFilenames(&filenames);
+  }
+
+  // Make sure we can read this.
+  const std::vector<LogFile> sorted_parts = SortParts(filenames);
+  EXPECT_TRUE(AllPartsMatchOutOfOrderDuration(sorted_parts));
+  auto result = ConfirmReadable(filenames);
+}
 
 // Tests that we log correctly as nodes connect slowly.
 TEST(MultinodeLoggerLoopTest, StaggeredConnect) {
diff --git a/aos/events/logging/multinode_logger_test_lib.cc b/aos/events/logging/multinode_logger_test_lib.cc
index ed62a2e..3bf6207 100644
--- a/aos/events/logging/multinode_logger_test_lib.cc
+++ b/aos/events/logging/multinode_logger_test_lib.cc
@@ -29,7 +29,8 @@
           configuration::GetNode(configuration, node->node()),
           nullptr,
           params,
-          file_strategy};
+          file_strategy,
+          nullptr};
 }
 
 std::unique_ptr<MultiNodeFilesLogNamer> LoggerState::MakeLogNamer(
@@ -56,12 +57,17 @@
       absl::StrCat("logger_sha1_", event_loop->node()->name()->str()));
   logger->set_logger_version(
       absl::StrCat("logger_version_", event_loop->node()->name()->str()));
-  event_loop->OnRun([this, logfile_base]() {
+  CHECK(start_timer == nullptr)
+      << ": Test fixture doesn't yet supporting starting a logger twice.";
+
+  // Use a timer for starting since OnRun can only happen at the actual startup.
+  start_timer = event_loop->AddTimer([this, logfile_base]() {
     std::unique_ptr<MultiNodeFilesLogNamer> namer = MakeLogNamer(logfile_base);
     log_namer = namer.get();
 
     logger->StartLogging(std::move(namer));
   });
+  start_timer->Schedule(event_loop->monotonic_now());
 }
 
 void LoggerState::AppendAllFilenames(std::vector<std::string> *filenames) {
diff --git a/aos/events/logging/multinode_logger_test_lib.h b/aos/events/logging/multinode_logger_test_lib.h
index 8f64f66..a8e5969 100644
--- a/aos/events/logging/multinode_logger_test_lib.h
+++ b/aos/events/logging/multinode_logger_test_lib.h
@@ -69,6 +69,7 @@
   MultiNodeFilesLogNamer *log_namer;
   CompressionParams params;
   FileStrategy file_strategy;
+  aos::TimerHandler *start_timer;
 
   void AppendAllFilenames(std::vector<std::string> *filenames);
 
@@ -76,13 +77,13 @@
 };
 
 constexpr std::string_view kCombinedConfigSha1() {
-  return "71eb8341221fbabefb4ddde43bcebf794fd5855e3ad77786a1db0f9e27a39091";
+  return "adf6a65be9b9a00d85ad1db4c78495e46d9c35b883ef95581c46222b2624d79d";
 }
 constexpr std::string_view kSplitConfigSha1() {
-  return "f61d45dc0bda026e852e2da9b3e5c2c7f1c89c9f7958cfba3d02e2c960416f04";
+  return "7998834e993bcf000c8f03f6fcc5cc63650fdbd1f42ff0a2d2bdbbf1182e3104";
 }
 constexpr std::string_view kReloggedSplitConfigSha1() {
-  return "3d8fd3d13955b517ee3d66a50b5e4dd7a13fd648f469d16910990418bcfc6beb";
+  return "5e800fdbaf6a088f33d5df42a58d803a33caa33eea269fef9e390b0306e9c11e";
 }
 
 LoggerState MakeLoggerState(NodeEventLoopFactory *node,
diff --git a/aos/events/logging/multinode_pingpong_reboot_reliable_only.json b/aos/events/logging/multinode_pingpong_reboot_reliable_only.json
new file mode 100644
index 0000000..cfe421d
--- /dev/null
+++ b/aos/events/logging/multinode_pingpong_reboot_reliable_only.json
@@ -0,0 +1,214 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    /* Logged on pi1 locally */
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.DynamicLogCommand",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.DynamicLogCommand",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "timestamp_logger": "NOT_LOGGED"
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "timestamp_logger": "NOT_LOGGED"
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/atest1/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1",
+      "frequency": 150
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/atest2/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1",
+      "frequency": 150
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/atest3/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1",
+      "frequency": 150
+    },
+    {
+      "name": "/atest1",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi2"],
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ],
+      "frequency": 150
+    },
+    {
+      "name": "/atest2",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi2"],
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "frequency": 150
+    },
+    {
+      "name": "/atest3",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "frequency": 150
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1",
+      "hostname": "raspberrypi",
+      "port": 9971
+    },
+    {
+      "name": "pi2",
+      "hostname": "raspberrypi2",
+      "port": 9971
+    }
+  ]
+}
diff --git a/aos/events/logging/realtime_replay_test.cc b/aos/events/logging/realtime_replay_test.cc
index 4118550..dd4aaf0 100644
--- a/aos/events/logging/realtime_replay_test.cc
+++ b/aos/events/logging/realtime_replay_test.cc
@@ -88,11 +88,10 @@
 
 TEST_F(RealtimeLoggerTest, RealtimeReplay) {
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(std::chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_separate_config(false);
     logger.set_polling_period(std::chrono::milliseconds(100));
@@ -123,11 +122,10 @@
 // is included on a single node config
 TEST_F(RealtimeLoggerTest, SingleNodeReplayChannels) {
   {
-    std::unique_ptr<EventLoop> logger_event_loop =
-        event_loop_factory_.MakeEventLoop("logger");
-
     event_loop_factory_.RunFor(std::chrono::milliseconds(95));
 
+    std::unique_ptr<EventLoop> logger_event_loop =
+        event_loop_factory_.MakeEventLoop("logger");
     Logger logger(logger_event_loop.get());
     logger.set_separate_config(false);
     logger.set_polling_period(std::chrono::milliseconds(100));
diff --git a/aos/events/shm_event_loop.cc b/aos/events/shm_event_loop.cc
index 7f06ee0..9d010b7 100644
--- a/aos/events/shm_event_loop.cc
+++ b/aos/events/shm_event_loop.cc
@@ -237,6 +237,7 @@
     ipc_lib::LocklessQueueReader::Result read_result = reader_.Read(
         queue_index.index(), &context_.monotonic_event_time,
         &context_.realtime_event_time, &context_.monotonic_remote_time,
+        &context_.monotonic_remote_transmit_time,
         &context_.realtime_remote_time, &context_.remote_queue_index,
         &context_.source_boot_uuid, &context_.size, copy_buffer, std::move(fn));
 
@@ -457,6 +458,7 @@
   Error DoSend(size_t length,
                aos::monotonic_clock::time_point monotonic_remote_time,
                aos::realtime_clock::time_point realtime_remote_time,
+               aos::monotonic_clock::time_point monotonic_remote_transmit_time,
                uint32_t remote_queue_index,
                const UUID &source_boot_uuid) override {
     shm_event_loop()->CheckCurrentThread();
@@ -464,9 +466,9 @@
         << ": Sent too big a message on "
         << configuration::CleanedChannelToString(channel());
     const auto result = lockless_queue_sender_.Send(
-        length, monotonic_remote_time, realtime_remote_time, remote_queue_index,
-        source_boot_uuid, &monotonic_sent_time_, &realtime_sent_time_,
-        &sent_queue_index_);
+        length, monotonic_remote_time, realtime_remote_time,
+        monotonic_remote_transmit_time, remote_queue_index, source_boot_uuid,
+        &monotonic_sent_time_, &realtime_sent_time_, &sent_queue_index_);
     CHECK_NE(result, ipc_lib::LocklessQueueSender::Result::INVALID_REDZONE)
         << ": Somebody wrote outside the buffer of their message on channel "
         << configuration::CleanedChannelToString(channel());
@@ -480,6 +482,7 @@
   Error DoSend(const void *msg, size_t length,
                aos::monotonic_clock::time_point monotonic_remote_time,
                aos::realtime_clock::time_point realtime_remote_time,
+               aos::monotonic_clock::time_point monotonic_remote_transmit_time,
                uint32_t remote_queue_index,
                const UUID &source_boot_uuid) override {
     shm_event_loop()->CheckCurrentThread();
@@ -488,8 +491,9 @@
         << configuration::CleanedChannelToString(channel());
     const auto result = lockless_queue_sender_.Send(
         reinterpret_cast<const char *>(msg), length, monotonic_remote_time,
-        realtime_remote_time, remote_queue_index, source_boot_uuid,
-        &monotonic_sent_time_, &realtime_sent_time_, &sent_queue_index_);
+        realtime_remote_time, monotonic_remote_transmit_time,
+        remote_queue_index, source_boot_uuid, &monotonic_sent_time_,
+        &realtime_sent_time_, &sent_queue_index_);
 
     CHECK_NE(result, ipc_lib::LocklessQueueSender::Result::INVALID_REDZONE)
         << ": Somebody wrote outside the buffer of their message on "
diff --git a/aos/events/shm_event_loop_test.cc b/aos/events/shm_event_loop_test.cc
index 4db0c7d..bae834a 100644
--- a/aos/events/shm_event_loop_test.cc
+++ b/aos/events/shm_event_loop_test.cc
@@ -25,12 +25,12 @@
     }
 
     // Clean up anything left there before.
-    unlink((FLAGS_shm_base + "/test/aos.TestMessage.v6").c_str());
-    unlink((FLAGS_shm_base + "/test1/aos.TestMessage.v6").c_str());
-    unlink((FLAGS_shm_base + "/test2/aos.TestMessage.v6").c_str());
-    unlink((FLAGS_shm_base + "/test2/aos.TestMessage.v6").c_str());
-    unlink((FLAGS_shm_base + "/aos/aos.timing.Report.v6").c_str());
-    unlink((FLAGS_shm_base + "/aos/aos.logging.LogMessageFbs.v6").c_str());
+    unlink((FLAGS_shm_base + "/test/aos.TestMessage.v7").c_str());
+    unlink((FLAGS_shm_base + "/test1/aos.TestMessage.v7").c_str());
+    unlink((FLAGS_shm_base + "/test2/aos.TestMessage.v7").c_str());
+    unlink((FLAGS_shm_base + "/test2/aos.TestMessage.v7").c_str());
+    unlink((FLAGS_shm_base + "/aos/aos.timing.Report.v7").c_str());
+    unlink((FLAGS_shm_base + "/aos/aos.logging.LogMessageFbs.v7").c_str());
   }
 
   ~ShmEventLoopTestFactory() { FLAGS_override_hostname = ""; }
diff --git a/aos/events/simulated_event_loop.cc b/aos/events/simulated_event_loop.cc
index 5cb67b4..1b05841 100644
--- a/aos/events/simulated_event_loop.cc
+++ b/aos/events/simulated_event_loop.cc
@@ -356,18 +356,21 @@
 
   Error DoSend(size_t length, monotonic_clock::time_point monotonic_remote_time,
                realtime_clock::time_point realtime_remote_time,
+               monotonic_clock::time_point monotonic_remote_transmit_time,
                uint32_t remote_queue_index,
                const UUID &source_boot_uuid) override;
 
   Error DoSend(const void *msg, size_t size,
                monotonic_clock::time_point monotonic_remote_time,
                realtime_clock::time_point realtime_remote_time,
+               monotonic_clock::time_point monotonic_remote_transmit_time,
                uint32_t remote_queue_index,
                const UUID &source_boot_uuid) override;
 
   Error DoSend(const SharedSpan data,
                aos::monotonic_clock::time_point monotonic_remote_time,
                aos::realtime_clock::time_point realtime_remote_time,
+               monotonic_clock::time_point monotonic_remote_transmit_time,
                uint32_t remote_queue_index,
                const UUID &source_boot_uuid) override;
 
@@ -408,7 +411,8 @@
 
     CHECK(!fell_behind_) << ": Got behind on "
                          << configuration::StrippedChannelToString(
-                                simulated_channel_->channel());
+                                simulated_channel_->channel())
+                         << " on " << NodeName(event_loop()->node());
 
     if (fn) {
       Context context = msgs_.front()->context;
@@ -857,8 +861,13 @@
              ->emplace(SimpleChannel(channel),
                        std::unique_ptr<SimulatedChannel>(new SimulatedChannel(
                            channel,
+                           // There are a lot of tests which assume that 100 hz
+                           // messages can actually be sent out at 100 hz and
+                           // forwarded.  The jitter in wakeups causes small
+                           // variation in timing.  Ignore that.
                            configuration::ChannelStorageDuration(
-                               configuration(), channel),
+                               configuration(), channel) -
+                               send_delay(),
                            scheduler_)))
              .first;
   }
@@ -1038,7 +1047,7 @@
   // Remove times that are greater than or equal to a channel_storage_duration_
   // ago
   while (!last_times_.empty() &&
-         (now - last_times_.front() >= channel_storage_duration_)) {
+         (now >= channel_storage_duration_ + last_times_.front())) {
     last_times_.pop();
   }
 
@@ -1101,6 +1110,7 @@
 RawSender::Error SimulatedSender::DoSend(
     size_t length, monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
+    monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid) {
   // The allocations in here are due to infrastructure and don't count in the
   // no mallocs in RT code.
@@ -1120,6 +1130,8 @@
   message_->context.realtime_event_time = simulated_event_loop_->realtime_now();
   message_->context.realtime_remote_time = realtime_remote_time;
   message_->context.source_boot_uuid = source_boot_uuid;
+  message_->context.monotonic_remote_transmit_time =
+      monotonic_remote_transmit_time;
   CHECK_LE(length, message_->context.size);
   message_->context.size = length;
 
@@ -1131,19 +1143,14 @@
     VLOG(1) << simulated_event_loop_->distributed_now() << " "
             << NodeName(simulated_event_loop_->node())
             << simulated_event_loop_->monotonic_now() << " "
-            << simulated_event_loop_->name()
-            << "\nMessages were sent too fast:\n"
-            << "For channel: "
-            << configuration::CleanedChannelToString(
-                   simulated_channel_->channel())
-            << '\n'
-            << "Tried to send more than " << simulated_channel_->queue_size()
+            << simulated_event_loop_->name() << "   -> SentTooFast "
+            << configuration::StrippedChannelToString(channel())
+            << ", Tried to send more than " << simulated_channel_->queue_size()
             << " (queue size) messages in the last "
             << std::chrono::duration<double>(
                    simulated_channel_->channel_storage_duration())
                    .count()
-            << " seconds (channel storage duration)"
-            << "\n\n";
+            << " seconds (channel storage duration)";
     return Error::kMessagesSentTooFast;
   }
 
@@ -1162,6 +1169,7 @@
     const void *msg, size_t size,
     monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
+    monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid) {
   CHECK_LE(size, this->size())
       << ": Attempting to send too big a message on "
@@ -1176,12 +1184,14 @@
   memcpy(mutable_span.data(), msg, size);
 
   return DoSend(size, monotonic_remote_time, realtime_remote_time,
-                remote_queue_index, source_boot_uuid);
+                monotonic_remote_transmit_time, remote_queue_index,
+                source_boot_uuid);
 }
 
 RawSender::Error SimulatedSender::DoSend(
     const SharedSpan data, monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
+    monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid) {
   CHECK_LE(data->size(), this->size())
       << ": Attempting to send too big a message on "
@@ -1192,7 +1202,8 @@
   message_ = SimulatedMessage::Make(simulated_channel_, data);
 
   return DoSend(data->size(), monotonic_remote_time, realtime_remote_time,
-                remote_queue_index, source_boot_uuid);
+                monotonic_remote_transmit_time, remote_queue_index,
+                source_boot_uuid);
 }
 
 SimulatedTimerHandler::SimulatedTimerHandler(
@@ -1604,6 +1615,8 @@
     result->SkipTimingReport();
   }
 
+  // TODO(austin): You shouldn't be able to make an event loop before t=0...
+
   VLOG(1) << scheduler_.distributed_now() << " " << NodeName(node())
           << monotonic_now() << " MakeEventLoop(\"" << result->name() << "\")";
   return result;
diff --git a/aos/events/simulated_event_loop.h b/aos/events/simulated_event_loop.h
index 6d88aa6..e976544 100644
--- a/aos/events/simulated_event_loop.h
+++ b/aos/events/simulated_event_loop.h
@@ -245,6 +245,10 @@
   template <class Main, class... Args>
   Main *AlwaysStart(std::string_view name, Args &&...args);
 
+  // Stops an application given the pointer to the application started.
+  template <class Main>
+  void Stop(Main *application);
+
   // Returns the simulated network delay for messages forwarded between nodes.
   std::chrono::nanoseconds network_delay() const {
     return factory_->network_delay();
@@ -341,6 +345,8 @@
         : event_loop(node_factory->MakeEventLoop(name)) {}
     virtual ~Application() {}
 
+    virtual void *application() = 0;
+
     std::unique_ptr<EventLoop> event_loop;
   };
 
@@ -365,6 +371,8 @@
     }
     ~TypedApplication() override {}
 
+    void *application() override { return &main; }
+
     Main main;
   };
 
@@ -392,6 +400,16 @@
   return main_ptr;
 }
 
+template <class Main>
+void NodeEventLoopFactory::Stop(Main *application) {
+  auto it = std::remove_if(
+      applications_.begin(), applications_.end(),
+      [application](const std::unique_ptr<Application> &app) {
+        return app->application() == static_cast<void *>(application);
+      });
+  applications_.erase(it, applications_.end());
+}
+
 inline monotonic_clock::time_point NodeEventLoopFactory::monotonic_now() const {
   // TODO(austin): Confirm that time never goes backwards?
   return scheduler_.monotonic_now();
diff --git a/aos/events/simulated_event_loop_test.cc b/aos/events/simulated_event_loop_test.cc
index 050c9a3..745a273 100644
--- a/aos/events/simulated_event_loop_test.cc
+++ b/aos/events/simulated_event_loop_test.cc
@@ -7,6 +7,7 @@
 #include "gtest/gtest.h"
 
 #include "aos/events/event_loop_param_test.h"
+#include "aos/events/function_scheduler.h"
 #include "aos/events/logging/logger_generated.h"
 #include "aos/events/message_counter.h"
 #include "aos/events/ping_lib.h"
@@ -715,7 +716,7 @@
 
           EXPECT_EQ(connection->partial_deliveries(), 0);
           EXPECT_TRUE(connection->has_monotonic_offset());
-          EXPECT_EQ(connection->monotonic_offset(), 150000);
+          EXPECT_EQ(connection->monotonic_offset(), 100000);
           EXPECT_EQ(connection->connection_count(), 1u);
           EXPECT_EQ(connection->connected_since_time(), 0);
         }
@@ -735,7 +736,7 @@
         EXPECT_GT(connection->received_packets(), 50);
         EXPECT_EQ(connection->partial_deliveries(), 0);
         EXPECT_TRUE(connection->has_monotonic_offset());
-        EXPECT_EQ(connection->monotonic_offset(), 150000);
+        EXPECT_EQ(connection->monotonic_offset(), 100000);
         EXPECT_EQ(connection->connection_count(), 1u);
         EXPECT_EQ(connection->connected_since_time(), 0);
         ++pi2_client_statistics_count;
@@ -754,7 +755,7 @@
         EXPECT_GE(connection->received_packets(), 5);
         EXPECT_EQ(connection->partial_deliveries(), 0);
         EXPECT_TRUE(connection->has_monotonic_offset());
-        EXPECT_EQ(connection->monotonic_offset(), 150000);
+        EXPECT_EQ(connection->monotonic_offset(), 100000);
         EXPECT_EQ(connection->connection_count(), 1u);
         EXPECT_EQ(connection->connected_since_time(), 0);
         ++pi3_client_statistics_count;
@@ -798,8 +799,10 @@
         [pi1_timestamp_channel, ping_timestamp_channel, &ping_on_pi2_fetcher,
          &ping_on_pi1_fetcher, &pi1_on_pi2_timestamp_fetcher,
          &pi1_on_pi1_timestamp_fetcher, &simulated_event_loop_factory, pi2,
-         channel_index = channel.first](const RemoteMessage &header) {
-          VLOG(1) << aos::FlatbufferToJson(&header);
+         channel_index = channel.first,
+         channel_name = channel.second](const RemoteMessage &header) {
+          VLOG(1) << channel_name << " aos::message_bridge::RemoteMessage -> "
+                  << aos::FlatbufferToJson(&header);
           EXPECT_TRUE(header.has_boot_uuid());
           EXPECT_EQ(UUID::FromVector(header.boot_uuid()),
                     simulated_event_loop_factory.GetNodeEventLoopFactory(pi2)
@@ -811,6 +814,9 @@
               chrono::nanoseconds(header.realtime_sent_time()));
           const aos::monotonic_clock::time_point header_monotonic_remote_time(
               chrono::nanoseconds(header.monotonic_remote_time()));
+          const aos::monotonic_clock::time_point
+              header_monotonic_remote_transmit_time(
+                  chrono::nanoseconds(header.monotonic_remote_transmit_time()));
           const aos::realtime_clock::time_point header_realtime_remote_time(
               chrono::nanoseconds(header.realtime_remote_time()));
 
@@ -836,6 +842,9 @@
 
             pi1_context = &pi1_on_pi1_timestamp_fetcher.context();
             pi2_context = &pi1_on_pi2_timestamp_fetcher.context();
+
+            EXPECT_EQ(header_monotonic_remote_transmit_time,
+                      pi2_context->monotonic_remote_time);
           } else if (header.channel_index() == ping_timestamp_channel) {
             // Find the forwarded message.
             while (ping_on_pi2_fetcher.context().monotonic_event_time <
@@ -851,6 +860,10 @@
 
             pi1_context = &ping_on_pi1_fetcher.context();
             pi2_context = &ping_on_pi2_fetcher.context();
+
+            EXPECT_EQ(header_monotonic_remote_transmit_time,
+                      pi2_context->monotonic_event_time -
+                          simulated_event_loop_factory.network_delay());
           } else {
             LOG(FATAL) << "Unknown channel";
           }
@@ -868,6 +881,8 @@
                     header_realtime_remote_time);
           EXPECT_EQ(pi2_context->monotonic_remote_time,
                     header_monotonic_remote_time);
+          EXPECT_EQ(pi2_context->monotonic_remote_transmit_time,
+                    header_monotonic_remote_transmit_time);
 
           // Confirm the forwarded message also matches the source message.
           EXPECT_EQ(pi1_context->queue_index, header.remote_queue_index());
@@ -2195,7 +2210,15 @@
     ::std::unique_ptr<EventLoop> ping_event_loop = pi2->MakeEventLoop("pong");
     aos::Fetcher<examples::Ping> fetcher =
         ping_event_loop->MakeFetcher<examples::Ping>("/reliable");
-    EXPECT_TRUE(fetcher.Fetch());
+    ASSERT_TRUE(fetcher.Fetch());
+    EXPECT_EQ(fetcher.context().monotonic_remote_time,
+              monotonic_clock::epoch());
+    // Message bridge picks up the Ping message immediately on reboot.
+    EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+              monotonic_clock::epoch());
+    EXPECT_EQ(fetcher.context().monotonic_event_time,
+              monotonic_clock::epoch() + factory.network_delay());
+    ASSERT_FALSE(fetcher.Fetch());
   }
 
   factory.RunFor(chrono::seconds(1));
@@ -2204,7 +2227,15 @@
     ::std::unique_ptr<EventLoop> ping_event_loop = pi2->MakeEventLoop("pong");
     aos::Fetcher<examples::Ping> fetcher =
         ping_event_loop->MakeFetcher<examples::Ping>("/reliable");
-    EXPECT_TRUE(fetcher.Fetch());
+    ASSERT_TRUE(fetcher.Fetch());
+    EXPECT_EQ(fetcher.context().monotonic_remote_time,
+              monotonic_clock::epoch());
+    // Message bridge picks up the Ping message immediately on reboot.
+    EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+              monotonic_clock::epoch() + chrono::seconds(1));
+    EXPECT_EQ(fetcher.context().monotonic_event_time,
+              monotonic_clock::epoch() + factory.network_delay());
+    ASSERT_FALSE(fetcher.Fetch());
   }
   EXPECT_NE(pi2_boot_uuid, pi2->boot_uuid());
 }
@@ -2260,6 +2291,8 @@
               monotonic_clock::epoch() + factory.network_delay());
     EXPECT_EQ(fetcher.context().monotonic_remote_time,
               monotonic_clock::epoch());
+    EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+              monotonic_clock::epoch() + chrono::seconds(1));
   }
   {
     ::std::unique_ptr<EventLoop> pi1_event_loop = pi1->MakeEventLoop("pong");
@@ -2271,6 +2304,8 @@
                   factory.network_delay());
     EXPECT_EQ(fetcher.context().monotonic_remote_time,
               monotonic_clock::epoch() - std::chrono::seconds(1));
+    EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+              monotonic_clock::epoch());
   }
 }
 
@@ -2461,4 +2496,525 @@
   }
 }
 
+// Struct to capture the expected time a message should be received (and it's
+// value).  This is from the perspective of the node receiving the message.
+struct ExpectedTimestamps {
+  // The time that the message was published on the sending node's monotonic
+  // clock.
+  monotonic_clock::time_point remote_time;
+  // The time that the message was virtually transmitted over the virtual
+  // network on the sending node's monotonic clock.
+  monotonic_clock::time_point remote_transmit_time;
+  // The time that the message was received on the receiving node's clock.
+  monotonic_clock::time_point event_time;
+  // The value inside the message.
+  int value;
+};
+
+// Tests that rapidly sent messages get timestamped correctly.
+TEST(SimulatedEventLoopTest, TransmitTimestamps) {
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(
+          ArtifactPath("aos/events/multinode_pingpong_test_split_config.json"));
+
+  message_bridge::TestingTimeConverter time(
+      configuration::NodesCount(&config.message()));
+  SimulatedEventLoopFactory factory(&config.message());
+  factory.SetTimeConverter(&time);
+  time.StartEqual();
+
+  NodeEventLoopFactory *pi1 = factory.GetNodeEventLoopFactory("pi1");
+  NodeEventLoopFactory *pi2 = factory.GetNodeEventLoopFactory("pi2");
+
+  ::std::unique_ptr<EventLoop> ping_event_loop = pi2->MakeEventLoop("pong");
+  aos::Fetcher<examples::Ping> fetcher =
+      ping_event_loop->MakeFetcher<examples::Ping>("/reliable");
+  EXPECT_FALSE(fetcher.Fetch());
+
+  {
+    ::std::unique_ptr<EventLoop> ping_event_loop = pi1->MakeEventLoop("ping");
+    FunctionScheduler run_at(ping_event_loop.get());
+    aos::Sender<examples::Ping> test_message_sender =
+        ping_event_loop->MakeSender<examples::Ping>("/reliable");
+    aos::monotonic_clock::time_point now = ping_event_loop->monotonic_now();
+    for (const std::chrono::nanoseconds dt :
+         {chrono::microseconds(5000), chrono::microseconds(1),
+          chrono::microseconds(2), chrono::microseconds(70),
+          chrono::microseconds(63), chrono::microseconds(140)}) {
+      now += dt;
+      run_at.ScheduleAt([&]() { SendPing(&test_message_sender, 1); }, now);
+    }
+
+    now += chrono::milliseconds(10);
+
+    factory.RunFor(now - ping_event_loop->monotonic_now());
+  }
+
+  const monotonic_clock::time_point e = monotonic_clock::epoch();
+  const chrono::nanoseconds send_delay = factory.send_delay();
+  const chrono::nanoseconds network_delay = factory.network_delay();
+
+  const std::vector<ExpectedTimestamps> expected_values = {
+      // First message shows up after wakeup + network delay as expected.
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(5000),
+          .remote_transmit_time = e + chrono::microseconds(5000) + send_delay,
+          .event_time =
+              e + chrono::microseconds(5000) + send_delay + network_delay,
+          .value = 1,
+      },
+      // Next message is close enough that it gets picked up at the same wakeup.
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(5001),
+          .remote_transmit_time = e + chrono::microseconds(5000) + send_delay,
+          .event_time =
+              e + chrono::microseconds(5000) + send_delay + network_delay,
+          .value = 1,
+      },
+      // Same for the third.
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(5003),
+          .remote_transmit_time = e + chrono::microseconds(5000) + send_delay,
+          .event_time =
+              e + chrono::microseconds(5000) + send_delay + network_delay,
+          .value = 1,
+      },
+      // Fourth waits long enough to do the right thing.
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(5073),
+          .remote_transmit_time = e + chrono::microseconds(5073) + send_delay,
+          .event_time =
+              e + chrono::microseconds(5073) + send_delay + network_delay,
+          .value = 1,
+      },
+      // Fifth waits long enough to do the right thing as well (but kicks off
+      // while the fourth is in flight over the network).
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(5136),
+          .remote_transmit_time = e + chrono::microseconds(5136) + send_delay,
+          .event_time =
+              e + chrono::microseconds(5136) + send_delay + network_delay,
+          .value = 1,
+      },
+      // Sixth waits long enough to do the right thing as well (but kicks off
+      // while the fifth is in flight over the network and has almost landed).
+      // The timer wakeup for the Timestamp message coming back will find the
+      // sixth message a little bit early.
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(5276),
+          .remote_transmit_time = e + chrono::microseconds(5273) + send_delay,
+          .event_time =
+              e + chrono::microseconds(5273) + send_delay + network_delay,
+          .value = 1,
+      },
+  };
+
+  for (const ExpectedTimestamps value : expected_values) {
+    ASSERT_TRUE(fetcher.FetchNext());
+    EXPECT_EQ(fetcher.context().monotonic_remote_time, value.remote_time);
+    EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+              value.remote_transmit_time);
+    EXPECT_EQ(fetcher.context().monotonic_event_time, value.event_time);
+    EXPECT_EQ(fetcher->value(), value.value);
+  }
+
+  ASSERT_FALSE(fetcher.FetchNext());
+}
+
+// Tests that a reliable message gets forwarded if it was sent originally when
+// nodes were disconnected.
+TEST_F(SimulatedEventLoopDisconnectTest, ReliableMessageSendsOnConnect) {
+  time.StartEqual();
+  factory.SkipTimingReport();
+  factory.DisableStatistics();
+
+  NodeEventLoopFactory *pi1 = factory.GetNodeEventLoopFactory("pi1");
+  NodeEventLoopFactory *pi2 = factory.GetNodeEventLoopFactory("pi2");
+
+  // Fully disconnect the nodes.
+  pi1->Disconnect(pi2->node());
+  pi2->Disconnect(pi1->node());
+
+  std::unique_ptr<aos::EventLoop> pi2_event_loop =
+      pi2->MakeEventLoop("fetcher");
+  aos::Fetcher<examples::Ping> pi2_reliable_fetcher =
+      pi2_event_loop->MakeFetcher<examples::Ping>("/reliable");
+
+  factory.RunFor(chrono::milliseconds(100));
+
+  {
+    std::unique_ptr<aos::EventLoop> pi1_event_loop =
+        pi1->MakeEventLoop("sender");
+    aos::Sender<examples::Ping> pi1_reliable_sender =
+        pi1_event_loop->MakeSender<examples::Ping>("/reliable");
+    FunctionScheduler run_at(pi1_event_loop.get());
+    aos::monotonic_clock::time_point now = pi1_event_loop->monotonic_now();
+    for (int i = 0; i < 100; ++i) {
+      run_at.ScheduleAt([&, i = i]() { SendPing(&pi1_reliable_sender, i); },
+                        now);
+      now += chrono::milliseconds(100);
+    }
+    now += chrono::milliseconds(50);
+
+    factory.RunFor(now - pi1_event_loop->monotonic_now());
+  }
+
+  ASSERT_FALSE(pi2_reliable_fetcher.Fetch());
+
+  pi1->Connect(pi2->node());
+  pi2->Connect(pi1->node());
+
+  factory.RunFor(chrono::milliseconds(1));
+
+  ASSERT_TRUE(pi2_reliable_fetcher.Fetch());
+  ASSERT_EQ(pi2_reliable_fetcher.context().monotonic_remote_time,
+            monotonic_clock::epoch() + chrono::milliseconds(10000));
+  ASSERT_EQ(pi2_reliable_fetcher.context().monotonic_remote_transmit_time,
+            monotonic_clock::epoch() + chrono::milliseconds(10150));
+  ASSERT_EQ(pi2_reliable_fetcher.context().monotonic_event_time,
+            monotonic_clock::epoch() + chrono::milliseconds(10150) +
+                factory.network_delay());
+  ASSERT_EQ(pi2_reliable_fetcher->value(), 99);
+
+  // TODO(austin): Verify that the dropped packet count increases.
+
+  ASSERT_FALSE(pi2_reliable_fetcher.Fetch());
+}
+
+// Tests that if we disconnect while a message is in various states of being
+// queued, it gets either dropped or sent as expected.
+TEST_F(SimulatedEventLoopDisconnectTest, MessageInFlightDuringDisconnect) {
+  time.StartEqual();
+  factory.SkipTimingReport();
+  factory.DisableStatistics();
+
+  NodeEventLoopFactory *pi1 = factory.GetNodeEventLoopFactory("pi1");
+  NodeEventLoopFactory *pi2 = factory.GetNodeEventLoopFactory("pi2");
+
+  std::unique_ptr<aos::EventLoop> pi1_event_loop = pi1->MakeEventLoop("sender");
+
+  std::unique_ptr<aos::EventLoop> pi2_event_loop =
+      pi2->MakeEventLoop("fetcher");
+  aos::Fetcher<examples::Ping> fetcher =
+      pi2_event_loop->MakeFetcher<examples::Ping>("/unreliable");
+
+  ASSERT_FALSE(fetcher.Fetch());
+
+  aos::monotonic_clock::time_point now = pi1_event_loop->monotonic_now();
+  {
+    FunctionScheduler run_at(pi1_event_loop.get());
+    aos::Sender<examples::Ping> pi1_sender =
+        pi1_event_loop->MakeSender<examples::Ping>("/unreliable");
+
+    int i = 0;
+    for (const std::chrono::nanoseconds dt :
+         {chrono::microseconds(5000), chrono::microseconds(1),
+          chrono::microseconds(2), chrono::microseconds(70),
+          chrono::microseconds(63), chrono::microseconds(140),
+          chrono::microseconds(160)}) {
+      run_at.ScheduleAt(
+          [&]() {
+            pi1->Connect(pi2->node());
+            pi2->Connect(pi1->node());
+          },
+          now);
+
+      now += chrono::milliseconds(100);
+
+      run_at.ScheduleAt([&, i = i]() { SendPing(&pi1_sender, i); }, now);
+
+      now += dt;
+
+      run_at.ScheduleAt(
+          [&]() {
+            // Fully disconnect the nodes.
+            pi1->Disconnect(pi2->node());
+            pi2->Disconnect(pi1->node());
+          },
+          now);
+
+      now += chrono::milliseconds(100) - dt;
+      ++i;
+    }
+
+    factory.RunFor(now - pi1_event_loop->monotonic_now());
+  }
+
+  const monotonic_clock::time_point e = monotonic_clock::epoch();
+  const chrono::nanoseconds send_delay = factory.send_delay();
+  const chrono::nanoseconds network_delay = factory.network_delay();
+
+  const std::vector<ExpectedTimestamps> expected_values = {
+      ExpectedTimestamps{
+          .remote_time = e + chrono::milliseconds(100),
+          .remote_transmit_time = e + chrono::milliseconds(100) + send_delay,
+          .event_time =
+              e + chrono::milliseconds(100) + send_delay + network_delay,
+          .value = 0,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::milliseconds(1300),
+          .remote_transmit_time = e + chrono::milliseconds(1300) + send_delay,
+          .event_time =
+              e + chrono::milliseconds(1300) + send_delay + network_delay,
+          .value = 6,
+      },
+  };
+
+  for (const ExpectedTimestamps value : expected_values) {
+    ASSERT_TRUE(fetcher.FetchNext());
+    EXPECT_EQ(fetcher.context().monotonic_remote_time, value.remote_time);
+    EXPECT_EQ(fetcher.context().monotonic_remote_transmit_time,
+              value.remote_transmit_time);
+    EXPECT_EQ(fetcher.context().monotonic_event_time, value.event_time);
+    EXPECT_EQ(fetcher->value(), value.value);
+  }
+
+  // TODO(austin): Verify that the dropped packet count increases.
+
+  ASSERT_FALSE(fetcher.Fetch());
+}
+
+class PingLogger {
+ public:
+  PingLogger(aos::EventLoop *event_loop, std::string_view channel,
+             std::vector<std::pair<aos::Context, int>> *msgs)
+      : event_loop_(event_loop),
+        fetcher_(event_loop_->MakeFetcher<examples::Ping>(channel)),
+        msgs_(msgs) {
+    event_loop_->OnRun([this]() { CHECK(!fetcher_.Fetch()); });
+  }
+
+  ~PingLogger() {
+    while (fetcher_.FetchNext()) {
+      msgs_->emplace_back(fetcher_.context(), fetcher_->value());
+    }
+  }
+
+ private:
+  aos::EventLoop *event_loop_;
+  aos::Fetcher<examples::Ping> fetcher_;
+  std::vector<std::pair<aos::Context, int>> *msgs_;
+};
+
+// Tests that rebooting while a message is in flight works as expected.
+TEST_F(SimulatedEventLoopDisconnectTest, MessageInFlightDuringReboot) {
+  time.StartEqual();
+  for (int i = 0; i < 8; ++i) {
+    time.RebootAt(1, distributed_clock::epoch() + chrono::seconds(10 * i));
+  }
+
+  factory.SkipTimingReport();
+  factory.DisableStatistics();
+
+  NodeEventLoopFactory *pi1 = factory.GetNodeEventLoopFactory("pi1");
+  NodeEventLoopFactory *pi2 = factory.GetNodeEventLoopFactory("pi2");
+
+  std::unique_ptr<aos::EventLoop> pi1_event_loop = pi1->MakeEventLoop("sender");
+
+  aos::monotonic_clock::time_point now = pi1_event_loop->monotonic_now();
+  FunctionScheduler run_at(pi1_event_loop.get());
+  aos::Sender<examples::Ping> pi1_sender =
+      pi1_event_loop->MakeSender<examples::Ping>("/unreliable");
+
+  int i = 0;
+  for (const std::chrono::nanoseconds dt :
+       {chrono::microseconds(5000), chrono::microseconds(1),
+        chrono::microseconds(2), chrono::microseconds(70),
+        chrono::microseconds(63), chrono::microseconds(140),
+        chrono::microseconds(160)}) {
+    run_at.ScheduleAt([&, i = i]() { SendPing(&pi1_sender, i); },
+                      now + chrono::seconds(10) - dt);
+
+    now += chrono::seconds(10);
+    ++i;
+  }
+
+  std::vector<std::pair<aos::Context, int>> msgs;
+
+  pi2->OnStartup([pi2, &msgs]() {
+    pi2->AlwaysStart<PingLogger>("ping_logger", "/unreliable", &msgs);
+  });
+
+  factory.RunFor(now - pi1_event_loop->monotonic_now() + chrono::seconds(10));
+
+  const monotonic_clock::time_point e = monotonic_clock::epoch();
+  const chrono::nanoseconds send_delay = factory.send_delay();
+  const chrono::nanoseconds network_delay = factory.network_delay();
+
+  const std::vector<ExpectedTimestamps> expected_values = {
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(9995000),
+          .remote_transmit_time =
+              e + chrono::microseconds(9995000) + send_delay,
+          .event_time =
+              e + chrono::microseconds(9995000) + send_delay + network_delay,
+          .value = 0,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(19999999),
+          .remote_transmit_time =
+              e + chrono::microseconds(19999999) + send_delay,
+          .event_time =
+              e + chrono::microseconds(-1) + send_delay + network_delay,
+          .value = 1,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(29999998),
+          .remote_transmit_time =
+              e + chrono::microseconds(29999998) + send_delay,
+          .event_time =
+              e + chrono::microseconds(-2) + send_delay + network_delay,
+          .value = 2,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(69999840),
+          .remote_transmit_time =
+              e + chrono::microseconds(69999840) + send_delay,
+          .event_time =
+              e + chrono::microseconds(9999840) + send_delay + network_delay,
+          .value = 6,
+      },
+  };
+
+  ASSERT_EQ(msgs.size(), expected_values.size());
+
+  for (size_t i = 0; i < msgs.size(); ++i) {
+    EXPECT_EQ(msgs[i].first.monotonic_remote_time,
+              expected_values[i].remote_time);
+    EXPECT_EQ(msgs[i].first.monotonic_remote_transmit_time,
+              expected_values[i].remote_transmit_time);
+    EXPECT_EQ(msgs[i].first.monotonic_event_time,
+              expected_values[i].event_time);
+    EXPECT_EQ(msgs[i].second, expected_values[i].value);
+  }
+
+  // TODO(austin): Verify that the dropped packet count increases.
+}
+
+// Tests that rebooting while a message is in flight works as expected.
+TEST_F(SimulatedEventLoopDisconnectTest, ReliableMessageInFlightDuringReboot) {
+  time.StartEqual();
+  for (int i = 0; i < 8; ++i) {
+    time.RebootAt(1, distributed_clock::epoch() + chrono::seconds(10 * i));
+  }
+
+  factory.SkipTimingReport();
+  factory.DisableStatistics();
+
+  NodeEventLoopFactory *pi1 = factory.GetNodeEventLoopFactory("pi1");
+  NodeEventLoopFactory *pi2 = factory.GetNodeEventLoopFactory("pi2");
+
+  std::unique_ptr<aos::EventLoop> pi1_event_loop = pi1->MakeEventLoop("sender");
+
+  aos::monotonic_clock::time_point now = pi1_event_loop->monotonic_now();
+  FunctionScheduler run_at(pi1_event_loop.get());
+  aos::Sender<examples::Ping> pi1_sender =
+      pi1_event_loop->MakeSender<examples::Ping>("/reliable");
+
+  int i = 0;
+  for (const std::chrono::nanoseconds dt :
+       {chrono::microseconds(5000), chrono::microseconds(1),
+        chrono::microseconds(2), chrono::microseconds(70),
+        chrono::microseconds(63), chrono::microseconds(140),
+        chrono::microseconds(160)}) {
+    run_at.ScheduleAt([&, i = i]() { SendPing(&pi1_sender, i); },
+                      now + chrono::seconds(10) - dt);
+
+    now += chrono::seconds(10);
+    ++i;
+  }
+
+  std::vector<std::pair<aos::Context, int>> msgs;
+
+  PingLogger *logger;
+  pi2->OnStartup([pi2, &msgs, &logger]() {
+    logger = pi2->AlwaysStart<PingLogger>("ping_logger", "/reliable", &msgs);
+  });
+
+  factory.RunFor(now - pi1_event_loop->monotonic_now() + chrono::seconds(10));
+
+  // Stop the logger to flush the last boot of data.
+  pi2->Stop(logger);
+
+  const monotonic_clock::time_point e = monotonic_clock::epoch();
+  const chrono::nanoseconds send_delay = factory.send_delay();
+  const chrono::nanoseconds network_delay = factory.network_delay();
+
+  // Verified using --vmodule=simulated_event_loop=1 and looking at the actual
+  // event times to confirm what should have been forwarded when.
+  const std::vector<ExpectedTimestamps> expected_values = {
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(9995000),
+          .remote_transmit_time =
+              e + chrono::microseconds(9995000) + send_delay,
+          .event_time =
+              e + chrono::microseconds(9995000) + send_delay + network_delay,
+          .value = 0,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(9995000),
+          .remote_transmit_time = e + chrono::microseconds(10000000),
+          .event_time = e + network_delay,
+          .value = 0,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(19999999),
+          .remote_transmit_time = e + chrono::microseconds(20000000),
+          .event_time = e + network_delay,
+          .value = 1,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(29999998),
+          .remote_transmit_time = e + chrono::microseconds(30000000),
+          .event_time = e + network_delay,
+          .value = 2,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(39999930),
+          .remote_transmit_time = e + chrono::microseconds(40000000),
+          .event_time = e + network_delay,
+          .value = 3,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(49999937),
+          .remote_transmit_time = e + chrono::microseconds(50000000),
+          .event_time = e + network_delay,
+          .value = 4,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(59999860),
+          .remote_transmit_time = e + chrono::microseconds(60000000),
+          .event_time = e + network_delay,
+          .value = 5,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(69999840),
+          .remote_transmit_time = e + chrono::microseconds(69999890),
+          .event_time = e + chrono::microseconds(9999890) + network_delay,
+          .value = 6,
+      },
+      ExpectedTimestamps{
+          .remote_time = e + chrono::microseconds(69999840),
+          .remote_transmit_time = e + chrono::microseconds(70000000),
+          .event_time = e + network_delay,
+          .value = 6,
+      },
+  };
+
+  ASSERT_EQ(msgs.size(), expected_values.size());
+
+  for (size_t i = 0; i < msgs.size(); ++i) {
+    EXPECT_EQ(msgs[i].first.monotonic_remote_time,
+              expected_values[i].remote_time);
+    EXPECT_EQ(msgs[i].first.monotonic_remote_transmit_time,
+              expected_values[i].remote_transmit_time);
+    EXPECT_EQ(msgs[i].first.monotonic_event_time,
+              expected_values[i].event_time);
+    EXPECT_EQ(msgs[i].second, expected_values[i].value);
+  }
+
+  // TODO(austin): Verify that the dropped packet count increases.
+}
+
 }  // namespace aos::testing
diff --git a/aos/events/simulated_network_bridge.cc b/aos/events/simulated_network_bridge.cc
index 5c97292..66c902f 100644
--- a/aos/events/simulated_network_bridge.cc
+++ b/aos/events/simulated_network_bridge.cc
@@ -1,6 +1,7 @@
 #include "aos/events/simulated_network_bridge.h"
 
 #include "absl/strings/str_cat.h"
+#include "glog/logging.h"
 
 #include "aos/configuration.h"
 #include "aos/events/event_loop.h"
@@ -12,8 +13,10 @@
 // This class delays messages forwarded between two factories.
 //
 // The basic design is that we need to use the distributed_clock to convert
-// monotonic times from the source to the destination node.  We also use a
-// fetcher to manage the queue of data, and a timer to schedule the sends.
+// monotonic times from the source to the destination node.  We use a list of
+// timestamps added each time a message is delivered to the server side to drive
+// the client side publishing.  This pulls the data from the fetcher to match
+// with the timestamps queued.
 class RawMessageDelayer {
  public:
   RawMessageDelayer(const Channel *channel, const Connection *connection,
@@ -41,7 +44,8 @@
   void SetFetchEventLoop(aos::EventLoop *fetch_event_loop,
                          MessageBridgeServerStatus *server_status,
                          ChannelTimestampSender *timestamp_loggers) {
-    sent_ = false;
+    // Clear out state when the source node restarts.
+    last_sent_ = TransmitTime();
     fetch_event_loop_ = fetch_event_loop;
     if (fetch_event_loop_) {
       fetcher_ = fetch_event_loop_->MakeRawFetcher(channel_);
@@ -50,7 +54,7 @@
     }
 
     server_status_ = server_status;
-    if (server_status) {
+    if (server_status_) {
       server_connection_ =
           server_status_->FindServerConnection(send_node_factory_->node());
       server_index_ = configuration::GetNodeIndex(
@@ -84,7 +88,6 @@
 
   void SetSendEventLoop(aos::EventLoop *send_event_loop,
                         MessageBridgeClientStatus *client_status) {
-    sent_ = false;
     send_event_loop_ = send_event_loop;
     if (send_event_loop_ && !forwarding_disabled_) {
       sender_ = send_event_loop_->MakeRawSender(channel_);
@@ -121,147 +124,264 @@
 
   const Channel *channel() const { return channel_; }
 
-  uint32_t time_to_live() {
+  // Returns true if the connection is reliable.
+  bool reliable() const { return time_to_live() == 0; }
+
+  uint32_t time_to_live() const {
     return configuration::ConnectionToNode(channel_, send_node_factory_->node())
         ->time_to_live();
   }
 
-  void ScheduleReliable() {
-    if (forwarding_disabled()) return;
+  std::string Name() const {
+    std::string result;
+    result +=
+        (fetch_event_loop_ ? fetch_event_loop_->node()->name()->string_view()
+                           : std::string_view("?"));
+    result += " -> ";
+    result +=
+        (send_event_loop_ ? send_event_loop_->node()->name()->string_view()
+                          : std::string_view("?"));
+    result += " ";
+    result += aos::configuration::StrippedChannelToString(channel());
+    return result;
+  }
 
+  // Schedules forwarding any reliable messages when a node boots.
+  void ScheduleReliable() {
+    if (forwarding_disabled()) {
+      return;
+    }
+
+    // There is no sending side awake, don't do work.
     if (!fetcher_) {
       return;
     }
-    if (fetcher_->context().data == nullptr || sent_) {
-      sent_ = !fetcher_->Fetch();
-    }
 
-    FetchNext();
-    if (fetcher_->context().data == nullptr || sent_) {
+    // The network connection is disconnected, forget about this message.  If
+    // this is a reliable message, it will get picked up in Connect() so we
+    // don't need to follow it here.
+    if (server_connection_->state() != State::CONNECTED) {
       return;
     }
 
-    // Send at startup.  It is the best we can do.
-    const monotonic_clock::time_point monotonic_delivered_time =
-        send_node_factory_->monotonic_now() +
-        send_node_factory_->network_delay();
+    // If there is no receiving side, bail.
+    if (!timer_) {
+      return;
+    }
 
-    CHECK_GE(monotonic_delivered_time, send_node_factory_->monotonic_now())
+    // We only want the newest message, grab it and see if there's anything to
+    // do.
+    fetcher_->Fetch();
+
+    // No data, bail.
+    if (fetcher_->context().data == nullptr) {
+      return;
+    }
+
+    // Now, we know we've got a message we need to deliver, mark it down.
+    QueueMessage(fetcher_->context().queue_index,
+                 fetcher_->context().monotonic_event_time,
+                 fetch_event_loop_->monotonic_now());
+
+    // Send at startup.  It is the best we can do.
+    const logger::BootTimestamp monotonic_delivery_time =
+        DeliveredTime(monotonic_remote_transmit_times_.front().transmit_time);
+
+    // This can only happen if a node reboots in under 100 uS.  That's crazy,
+    // CHECK for now and handle it if someone actually has a good need.
+    CHECK_EQ(monotonic_delivery_time.boot, send_node_factory_->boot_count());
+
+    CHECK_GE(monotonic_delivery_time.time, send_node_factory_->monotonic_now())
         << ": Trying to deliver message in the past on channel "
         << configuration::StrippedChannelToString(fetcher_->channel())
         << " to node " << send_event_loop_->node()->name()->string_view()
         << " sent from " << fetcher_->channel()->source_node()->string_view()
         << " at " << fetch_node_factory_->monotonic_now();
 
-    if (timer_) {
+    if (!timer_scheduled_) {
       server_status_->AddSentPacket(server_index_, channel_);
-      timer_->Schedule(monotonic_delivered_time);
+      timer_->Schedule(monotonic_delivery_time.time);
       timer_scheduled_ = true;
-    } else {
-      server_status_->AddDroppedPacket(server_index_, channel_);
-      sent_ = true;
     }
   }
 
-  bool timer_scheduled_ = false;
+  // Handles a message begin delivered to message_bridge_server, and either
+  // drops it or queues it up.
+  void MessageWatcherCallback(uint32_t sent_queue_index,
+                              monotonic_clock::time_point monotonic_sent_time,
+                              monotonic_clock::time_point transmit_time) {
+    if (server_connection_->state() != State::CONNECTED) {
+      server_status_->AddDroppedPacket(server_index_, channel_);
+      return;
+    }
+
+    QueueMessage(sent_queue_index, monotonic_sent_time, transmit_time);
+    Schedule();
+  }
+
+  void QueueMessage(uint32_t sent_queue_index,
+                    monotonic_clock::time_point monotonic_sent_time,
+                    monotonic_clock::time_point transmit_time) {
+    CHECK(!forwarding_disabled());
+
+    // When a reliable message gets queued, we can both receive the wakeup from
+    // the watcher, and from ScheduleReliable.  In that case, detect that it is
+    // already in the queue and deduplicate with it.
+    if (monotonic_remote_transmit_times_.size() > 0u) {
+      const TransmitTime back = monotonic_remote_transmit_times_
+          [monotonic_remote_transmit_times_.size() - 1];
+      if (back.sent_queue_index == sent_queue_index) {
+        CHECK_EQ(back.monotonic_sent_time, monotonic_sent_time) << this;
+        CHECK(reliable());
+        CHECK_LE(back.transmit_time, transmit_time) << this;
+        return;
+      }
+    }
+
+    // Capture the time this message was published over the network on the
+    // remote node
+    monotonic_remote_transmit_times_.push_back(TransmitTime{
+        .monotonic_sent_time = monotonic_sent_time,
+        .sent_queue_index = sent_queue_index,
+        .transmit_time = transmit_time,
+    });
+  }
+
+  // Handles this node connecting to the network.
+  void Connect() {
+    CHECK(fetcher_);
+
+    // We only send the last message.  Point the fetcher to the latest to handle
+    // getting too far behind.
+    fetcher_->Fetch();
+
+    // Unreliable messages aren't resent on reconnect.
+    if (!reliable()) {
+      return;
+    }
+
+    if (forwarding_disabled()) {
+      return;
+    }
+
+    // Ignore it if there is no data.
+    if (fetcher_->context().data == nullptr) {
+      return;
+    }
+
+    // See if the newest message got sent already.  If it hasn't, queue it up to
+    // be sent.
+    if (fetcher_->context().queue_index != last_sent_.sent_queue_index) {
+      QueueMessage(fetcher_->context().queue_index,
+                   fetcher_->context().monotonic_event_time,
+                   fetch_event_loop_->monotonic_now());
+    }
+
+    Schedule();
+  }
+
+  // Returns true if we know that this connection sends to the destination node.
+  // Returns false if the destination hasn't been constructed.
+  bool SendingTo(const Node *destination) {
+    return send_event_loop_ && send_event_loop_->node() == destination;
+  }
 
   // Kicks us to re-fetch and schedule the timer.
   void Schedule() {
     CHECK(!forwarding_disabled());
+    // Can't receive, bail.
     if (!fetcher_) {
       return;
     }
+
+    // Already scheduled, nothing to see here.
     if (timer_scheduled_) {
       return;
     }
-    FetchNext();
-    if (fetcher_->context().data == nullptr || sent_) {
+
+    // We've finally caught up, nothing to do.
+    if (monotonic_remote_transmit_times_.empty()) {
       return;
     }
 
-    // Compute the time to publish this message.
-    const monotonic_clock::time_point monotonic_delivered_time =
-        DeliveredTime(fetcher_->context());
+    const monotonic_clock::time_point transmit_time =
+        monotonic_remote_transmit_times_[0].transmit_time;
 
-    CHECK_GE(monotonic_delivered_time, send_node_factory_->monotonic_now())
-        << ": Trying to deliver message in the past on channel "
+    // Compute the time to publish this message.
+    const logger::BootTimestamp monotonic_delivery_time =
+        DeliveredTime(transmit_time);
+
+    // This should be published after the reboot.  Forget about it.
+    if (monotonic_delivery_time.boot != send_node_factory_->boot_count()) {
+      CHECK_GT(monotonic_delivery_time.boot, send_node_factory_->boot_count());
+
+      monotonic_remote_transmit_times_.erase(
+          monotonic_remote_transmit_times_.begin());
+      CHECK(monotonic_remote_transmit_times_.empty());
+      return;
+    }
+
+    CHECK_GE(monotonic_delivery_time.time, send_node_factory_->monotonic_now())
+        << ": " << this << " Trying to deliver message in the past on channel "
         << configuration::StrippedChannelToString(fetcher_->channel())
         << " to node " << send_event_loop_->node()->name()->string_view()
         << " sent from " << fetcher_->channel()->source_node()->string_view()
         << " at " << fetch_node_factory_->monotonic_now();
 
-    if (timer_) {
-      server_status_->AddSentPacket(server_index_, channel_);
-      timer_->Schedule(monotonic_delivered_time);
-      timer_scheduled_ = true;
-    } else {
-      server_status_->AddDroppedPacket(server_index_, channel_);
-      sent_ = true;
-      Schedule();
-    }
+    CHECK(timer_);
+    server_status_->AddSentPacket(server_index_, channel_);
+    timer_->Schedule(monotonic_delivery_time.time);
+    timer_scheduled_ = true;
   }
 
  private:
-  void FetchNext() {
-    CHECK(server_connection_);
-    // Keep pulling messages out of the fetcher until we find one in the future.
-    while (true) {
-      if (fetcher_->context().data == nullptr || sent_) {
-        sent_ = !fetcher_->FetchNext();
-      }
-      if (sent_) {
-        break;
-      }
-
-      if (server_connection_->state() != State::CONNECTED) {
-        sent_ = true;
-        server_status_->AddDroppedPacket(server_index_, channel_);
-        continue;
-      }
-
-      if (fetcher_->context().monotonic_event_time +
-                  send_node_factory_->network_delay() +
-                  send_node_factory_->send_delay() >
-              fetch_node_factory_->monotonic_now() ||
-          time_to_live() == 0) {
-        break;
-      }
-
-      // TODO(austin): Not cool.  We want to actually forward these.  This means
-      // we need a more sophisticated concept of what is running.
-      // TODO(james): This fails if multiple messages are sent on the same
-      // channel within the same callback.
-      LOG(WARNING) << "Not forwarding message on "
-                   << configuration::CleanedChannelToString(fetcher_->channel())
-                   << " because we aren't running.  Sent at "
-                   << fetcher_->context().monotonic_event_time << " now is "
-                   << fetch_node_factory_->monotonic_now();
-      sent_ = true;
-      server_status_->AddDroppedPacket(server_index_, channel_);
-    }
-  }
-
   // Actually sends the message, and reschedules.
   void Send() {
     timer_scheduled_ = false;
+
     CHECK(sender_);
     CHECK(client_status_);
+    CHECK(fetcher_);
+
+    CHECK(!monotonic_remote_transmit_times_.empty());
+    while (fetcher_->context().queue_index !=
+           monotonic_remote_transmit_times_.front().sent_queue_index) {
+      if (!fetcher_->FetchNext()) {
+        break;
+      }
+    }
+
+    // Confirm that the first element in the times list is ours, and pull the
+    // transmit time out of it.
+    CHECK_EQ(monotonic_remote_transmit_times_[0].monotonic_sent_time,
+             fetcher_->context().monotonic_event_time);
+    CHECK_EQ(monotonic_remote_transmit_times_[0].sent_queue_index,
+             fetcher_->context().queue_index);
+
+    const TransmitTime timestamp = monotonic_remote_transmit_times_[0];
+
+    monotonic_remote_transmit_times_.erase(
+        monotonic_remote_transmit_times_.begin());
+
     if (server_connection_->state() != State::CONNECTED) {
-      sent_ = true;
       Schedule();
       return;
     }
+
     // Fill out the send times.
     sender_->CheckOk(sender_->Send(
         fetcher_->context().data, fetcher_->context().size,
         fetcher_->context().monotonic_event_time,
-        fetcher_->context().realtime_event_time,
+        fetcher_->context().realtime_event_time, timestamp.transmit_time,
         fetcher_->context().queue_index, fetcher_->context().source_boot_uuid));
 
+    // Record that this got sent.
+    last_sent_ = timestamp;
+
     // And simulate message_bridge's offset recovery.
-    client_status_->SampleFilter(
-        client_index_, fetcher_->context().monotonic_event_time,
-        sender_->monotonic_sent_time(), fetcher_->context().source_boot_uuid);
+    client_status_->SampleFilter(client_index_, timestamp.transmit_time,
+                                 sender_->monotonic_sent_time(),
+                                 fetcher_->context().source_boot_uuid);
 
     client_connection_->mutate_received_packets(
         client_connection_->received_packets() + 1);
@@ -294,7 +414,8 @@
           fetcher_->context().realtime_event_time.time_since_epoch().count());
       message_header_builder.add_remote_queue_index(
           fetcher_->context().queue_index);
-
+      message_header_builder.add_monotonic_remote_transmit_time(
+          timestamp.transmit_time.time_since_epoch().count());
       message_header_builder.add_monotonic_sent_time(
           sender_->monotonic_sent_time().time_since_epoch().count());
       message_header_builder.add_realtime_sent_time(
@@ -311,7 +432,6 @@
       ScheduleTimestamp();
     }
 
-    sent_ = true;
     Schedule();
   }
 
@@ -354,15 +474,14 @@
   }
 
   // Converts from time on the sending node to time on the receiving node.
-  monotonic_clock::time_point DeliveredTime(const Context &context) const {
+  logger::BootTimestamp DeliveredTime(
+      const monotonic_clock::time_point transmit_time) const {
     const distributed_clock::time_point distributed_sent_time =
-        fetch_node_factory_->ToDistributedClock(context.monotonic_event_time);
+        fetch_node_factory_->ToDistributedClock(transmit_time);
 
     const logger::BootTimestamp t = send_node_factory_->FromDistributedClock(
-        distributed_sent_time + send_node_factory_->network_delay() +
-        send_node_factory_->send_delay());
-    CHECK_EQ(t.boot, send_node_factory_->boot_count());
-    return t.time;
+        distributed_sent_time + send_node_factory_->network_delay());
+    return t;
   }
 
   const Channel *channel_;
@@ -378,6 +497,7 @@
   aos::EventLoop *send_event_loop_ = nullptr;
   // Timer used to send.
   aos::TimerHandler *timer_ = nullptr;
+  bool timer_scheduled_ = false;
   // Timer used to send timestamps out.
   aos::TimerHandler *timestamp_timer_ = nullptr;
   // Time that the timer is scheduled for.  Used to track if it needs to be
@@ -391,8 +511,6 @@
 
   MessageBridgeServerStatus *server_status_ = nullptr;
   const size_t destination_node_index_;
-  // True if we have sent the message in the fetcher.
-  bool sent_ = false;
 
   ServerConnection *server_connection_ = nullptr;
   int server_index_ = -1;
@@ -403,6 +521,20 @@
   size_t channel_index_;
   aos::Sender<RemoteMessage> *timestamp_logger_ = nullptr;
 
+  struct TransmitTime {
+    monotonic_clock::time_point monotonic_sent_time = monotonic_clock::min_time;
+    uint32_t sent_queue_index = 0xffffffff;
+    monotonic_clock::time_point transmit_time = monotonic_clock::min_time;
+  };
+
+  // Stores the time the message was handed to the kernel to be published on
+  // the remote node over the network for all forwarded relevant messages.
+  std::vector<TransmitTime> monotonic_remote_transmit_times_;
+
+  // Stores the last message which was published.  This is used to know if we
+  // need to re-transmit something on reconnect or not.
+  TransmitTime last_sent_;
+
   struct Timestamp {
     Timestamp(FlatbufferDetachedBuffer<RemoteMessage> new_remote_message,
               monotonic_clock::time_point new_monotonic_timestamp_time)
@@ -440,9 +572,9 @@
 
           size_t node_index = 0;
           for (const std::optional<MessageBridgeServerStatus::NodeState>
-                   &connection : node_state->server_status->nodes()) {
+                   &connection : node_state->server_status_->nodes()) {
             if (connection.has_value()) {
-              node_state->server_status->ResetFilter(node_index);
+              node_state->server_status_->ResetFilter(node_index);
             }
             ++node_index;
           }
@@ -514,10 +646,14 @@
 
     if (channel == timestamp_channel) {
       source_event_loop->second.SetSendData(
-          [captured_delayers = delayers.get()]() {
+          [source_event_loop, captured_delayers = delayers.get()](
+              uint32_t sent_queue_index,
+              monotonic_clock::time_point monotonic_sent_time) {
             for (std::unique_ptr<RawMessageDelayer> &delayer :
                  captured_delayers->v) {
-              delayer->Schedule();
+              delayer->MessageWatcherCallback(
+                  sent_queue_index, monotonic_sent_time,
+                  source_event_loop->second.event_loop->monotonic_now());
             }
           });
     } else {
@@ -588,11 +724,21 @@
   it->second.EnableStatistics();
 }
 
+void SimulatedMessageBridge::State::MakeEventLoop() {
+  // Message bridge isn't the thing that should be catching sent-too-fast,
+  // and may need to be able to forward too-fast messages replayed from old
+  // logfiles.
+  SetEventLoop(node_factory_->MakeEventLoop(
+      "message_bridge", {NodeEventLoopFactory::CheckSentTooFast::kNo,
+                         NodeEventLoopFactory::ExclusiveSenders::kNo,
+                         {}}));
+}
+
 void SimulatedMessageBridge::State::SetEventLoop(
     std::unique_ptr<aos::EventLoop> loop) {
   if (!loop) {
     timestamp_loggers = ChannelTimestampSender(nullptr);
-    server_status.reset();
+    server_status_.reset();
     client_status.reset();
     for (RawMessageDelayer *source_delayer : source_delayers_) {
       source_delayer->SetFetchEventLoop(nullptr, nullptr, nullptr);
@@ -615,38 +761,42 @@
     // Don't register watchers if we know we aren't forwarding.
     if (watcher.second->disable_forwarding) continue;
     event_loop->MakeRawNoArgWatcher(
-        watcher.first, [captured_delayers = watcher.second](const Context &) {
+        watcher.first,
+        [this, captured_delayers = watcher.second](const Context &context) {
           // We might get told after registering, so don't forward at that point
           // too.
           for (std::unique_ptr<RawMessageDelayer> &delayer :
                captured_delayers->v) {
-            delayer->Schedule();
+            delayer->MessageWatcherCallback(context.queue_index,
+                                            context.monotonic_event_time,
+                                            event_loop->monotonic_now());
           }
         });
   }
 
   timestamp_loggers = ChannelTimestampSender(event_loop.get());
-  server_status = std::make_unique<MessageBridgeServerStatus>(event_loop.get());
+  server_status_ =
+      std::make_unique<MessageBridgeServerStatus>(event_loop.get());
   if (disable_statistics_) {
-    server_status->DisableStatistics(destroy_senders_ == DestroySenders::kYes);
+    server_status_->DisableStatistics(destroy_senders_ == DestroySenders::kYes);
   }
 
   {
     size_t node_index = 0;
     for (const std::optional<MessageBridgeServerStatus::NodeState> &connection :
-         server_status->nodes()) {
+         server_status_->nodes()) {
       if (connection.has_value()) {
         if (boot_uuids_[node_index] != UUID::Zero()) {
           switch (server_state_[node_index]) {
             case message_bridge::State::DISCONNECTED:
-              server_status->Disconnect(node_index);
+              server_status_->Disconnect(node_index);
               break;
             case message_bridge::State::CONNECTED:
-              server_status->Connect(node_index, event_loop->monotonic_now());
+              server_status_->Connect(node_index, event_loop->monotonic_now());
               break;
           }
         } else {
-          server_status->Disconnect(node_index);
+          server_status_->Disconnect(node_index);
         }
       }
       ++node_index;
@@ -655,11 +805,11 @@
 
   for (size_t i = 0; i < boot_uuids_.size(); ++i) {
     if (boot_uuids_[i] != UUID::Zero()) {
-      server_status->SetBootUUID(i, boot_uuids_[i]);
+      server_status_->SetBootUUID(i, boot_uuids_[i]);
     }
   }
   if (fn_) {
-    server_status->set_send_data(fn_);
+    server_status_->set_send_data(fn_);
   }
   client_status = std::make_unique<MessageBridgeClientStatus>(event_loop.get());
   if (disable_statistics_) {
@@ -724,7 +874,7 @@
   }
 
   for (RawMessageDelayer *source_delayer : source_delayers_) {
-    source_delayer->SetFetchEventLoop(event_loop.get(), server_status.get(),
+    source_delayer->SetFetchEventLoop(event_loop.get(), server_status_.get(),
                                       &timestamp_loggers);
   }
   for (RawMessageDelayer *destination_delayer : destination_delayers_) {
@@ -733,7 +883,7 @@
   }
   event_loop->OnRun([this]() {
     for (RawMessageDelayer *destination_delayer : destination_delayers_) {
-      if (destination_delayer->time_to_live() == 0) {
+      if (destination_delayer->reliable()) {
         destination_delayer->ScheduleReliable();
       }
     }
@@ -752,11 +902,117 @@
     // the message, then that would trigger the watchers in the delayers.
     // However, we so far have continued to support Sending while stopped....
     for (RawMessageDelayer *source_delayer : source_delayers_) {
-      if (source_delayer->time_to_live() == 0) {
+      if (source_delayer->reliable()) {
         source_delayer->ScheduleReliable();
       }
     }
   });
 }
 
+void SimulatedMessageBridge::State::SetSendData(
+    std::function<void(uint32_t, monotonic_clock::time_point)> fn) {
+  CHECK(!fn_);
+  fn_ = std::move(fn);
+  if (server_status_) {
+    server_status_->set_send_data(fn_);
+  }
+}
+
+void SimulatedMessageBridge::State::SetBootUUID(size_t node_index,
+                                                const UUID &boot_uuid) {
+  boot_uuids_[node_index] = boot_uuid;
+  const Node *node = node_factory_->configuration()->nodes()->Get(node_index);
+  if (server_status_) {
+    ServerConnection *connection = server_status_->FindServerConnection(node);
+    if (connection) {
+      if (boot_uuid == UUID::Zero()) {
+        server_status_->Disconnect(node_index);
+        server_status_->ResetFilter(node_index);
+      } else {
+        switch (server_state_[node_index]) {
+          case message_bridge::State::DISCONNECTED:
+            server_status_->Disconnect(node_index);
+            break;
+          case message_bridge::State::CONNECTED:
+            server_status_->Connect(node_index, event_loop->monotonic_now());
+            break;
+        }
+        server_status_->ResetFilter(node_index);
+        server_status_->SetBootUUID(node_index, boot_uuid);
+      }
+    }
+  }
+  if (client_status) {
+    const int client_index =
+        client_status->FindClientIndex(node->name()->string_view());
+    client_status->SampleReset(client_index);
+    if (boot_uuid == UUID::Zero()) {
+      client_status->Disconnect(client_index);
+    } else {
+      switch (client_state_[node_index]) {
+        case message_bridge::State::CONNECTED:
+          client_status->Connect(client_index);
+          break;
+        case message_bridge::State::DISCONNECTED:
+          client_status->Disconnect(client_index);
+          break;
+      }
+    }
+  }
+}
+
+void SimulatedMessageBridge::State::SetServerState(
+    const Node *destination, message_bridge::State state) {
+  const size_t node_index =
+      configuration::GetNodeIndex(node_factory_->configuration(), destination);
+  server_state_[node_index] = state;
+  if (server_status_) {
+    ServerConnection *connection =
+        server_status_->FindServerConnection(destination);
+    if (connection == nullptr) return;
+
+    if (state == connection->state()) {
+      return;
+    }
+    switch (state) {
+      case message_bridge::State::DISCONNECTED:
+        server_status_->Disconnect(node_index);
+        break;
+      case message_bridge::State::CONNECTED:
+        server_status_->Connect(node_index, event_loop->monotonic_now());
+        for (RawMessageDelayer *delayer : source_delayers_) {
+          if (delayer->SendingTo(destination)) {
+            delayer->Connect();
+          }
+        }
+        break;
+    }
+  }
+}
+
+void SimulatedMessageBridge::State::SetClientState(
+    const Node *source, message_bridge::State state) {
+  const size_t node_index =
+      configuration::GetNodeIndex(node_factory_->configuration(), source);
+  client_state_[node_index] = state;
+  if (client_status) {
+    const int client_index =
+        client_status->FindClientIndex(source->name()->string_view());
+    ClientConnection *connection = client_status->GetClientConnection(source);
+
+    // TODO(austin): Are there cases where we want to dedup 2 CONNECTED
+    // calls?
+    if (connection->state() != state) {
+      switch (state) {
+        case message_bridge::State::CONNECTED:
+          client_status->Connect(client_index);
+          break;
+        case message_bridge::State::DISCONNECTED:
+          client_status->Disconnect(client_index);
+          break;
+      }
+    }
+  }
+}
+
 }  // namespace aos::message_bridge
diff --git a/aos/events/simulated_network_bridge.h b/aos/events/simulated_network_bridge.h
index 14a7321..e850396 100644
--- a/aos/events/simulated_network_bridge.h
+++ b/aos/events/simulated_network_bridge.h
@@ -59,9 +59,9 @@
     void DisableStatistics(DestroySenders destroy_senders) {
       disable_statistics_ = true;
       destroy_senders_ = destroy_senders;
-      if (server_status) {
-        server_status->DisableStatistics(destroy_senders ==
-                                         DestroySenders::kYes);
+      if (server_status_) {
+        server_status_->DisableStatistics(destroy_senders ==
+                                          DestroySenders::kYes);
       }
       if (client_status) {
         client_status->DisableStatistics(destroy_senders ==
@@ -71,8 +71,8 @@
 
     void EnableStatistics() {
       disable_statistics_ = false;
-      if (server_status) {
-        server_status->EnableStatistics();
+      if (server_status_) {
+        server_status_->EnableStatistics();
       }
       if (client_status) {
         client_status->EnableStatistics();
@@ -86,121 +86,22 @@
       destination_delayers_.emplace_back(delayer);
     }
 
-    void MakeEventLoop() {
-      // Message bridge isn't the thing that should be catching sent-too-fast,
-      // and may need to be able to forward too-fast messages replayed from old
-      // logfiles.
-      SetEventLoop(node_factory_->MakeEventLoop(
-          "message_bridge", {NodeEventLoopFactory::CheckSentTooFast::kNo,
-                             NodeEventLoopFactory::ExclusiveSenders::kNo,
-                             {}}));
-    }
+    void MakeEventLoop();
 
     void SetEventLoop(std::unique_ptr<aos::EventLoop> loop);
 
-    void SetSendData(std::function<void()> fn) {
-      CHECK(!fn_);
-      fn_ = std::move(fn);
-      if (server_status) {
-        server_status->set_send_data(fn_);
-      }
-    }
+    void SetSendData(
+        std::function<void(uint32_t, monotonic_clock::time_point)> fn);
 
     void AddDelayerWatcher(const Channel *channel, DelayersVector *v) {
       delayer_watchers_.emplace_back(channel, v);
     }
 
-    void SetBootUUID(size_t node_index, const UUID &boot_uuid) {
-      boot_uuids_[node_index] = boot_uuid;
-      const Node *node =
-          node_factory_->configuration()->nodes()->Get(node_index);
-      if (server_status) {
-        ServerConnection *connection =
-            server_status->FindServerConnection(node);
-        if (connection) {
-          if (boot_uuid == UUID::Zero()) {
-            server_status->Disconnect(node_index);
-            server_status->ResetFilter(node_index);
-          } else {
-            switch (server_state_[node_index]) {
-              case message_bridge::State::DISCONNECTED:
-                server_status->Disconnect(node_index);
-                break;
-              case message_bridge::State::CONNECTED:
-                server_status->Connect(node_index, event_loop->monotonic_now());
-                break;
-            }
-            server_status->ResetFilter(node_index);
-            server_status->SetBootUUID(node_index, boot_uuid);
-          }
-        }
-      }
-      if (client_status) {
-        const int client_index =
-            client_status->FindClientIndex(node->name()->string_view());
-        client_status->SampleReset(client_index);
-        if (boot_uuid == UUID::Zero()) {
-          client_status->Disconnect(client_index);
-        } else {
-          switch (client_state_[node_index]) {
-            case message_bridge::State::CONNECTED:
-              client_status->Connect(client_index);
-              break;
-            case message_bridge::State::DISCONNECTED:
-              client_status->Disconnect(client_index);
-              break;
-          }
-        }
-      }
-    }
+    void SetBootUUID(size_t node_index, const UUID &boot_uuid);
 
-    void SetServerState(const Node *destination, message_bridge::State state) {
-      const size_t node_index = configuration::GetNodeIndex(
-          node_factory_->configuration(), destination);
-      server_state_[node_index] = state;
-      if (server_status) {
-        ServerConnection *connection =
-            server_status->FindServerConnection(destination);
-        if (connection == nullptr) return;
+    void SetServerState(const Node *destination, message_bridge::State state);
 
-        if (state == connection->state()) {
-          return;
-        }
-        switch (state) {
-          case message_bridge::State::DISCONNECTED:
-            server_status->Disconnect(node_index);
-            break;
-          case message_bridge::State::CONNECTED:
-            server_status->Connect(node_index, event_loop->monotonic_now());
-            break;
-        }
-      }
-    }
-
-    void SetClientState(const Node *source, message_bridge::State state) {
-      const size_t node_index =
-          configuration::GetNodeIndex(node_factory_->configuration(), source);
-      client_state_[node_index] = state;
-      if (client_status) {
-        const int client_index =
-            client_status->FindClientIndex(source->name()->string_view());
-        ClientConnection *connection =
-            client_status->GetClientConnection(source);
-
-        // TODO(austin): Are there cases where we want to dedup 2 CONNECTED
-        // calls?
-        if (connection->state() != state) {
-          switch (state) {
-            case message_bridge::State::CONNECTED:
-              client_status->Connect(client_index);
-              break;
-            case message_bridge::State::DISCONNECTED:
-              client_status->Disconnect(client_index);
-              break;
-          }
-        }
-      }
-    }
+    void SetClientState(const Node *source, message_bridge::State state);
 
     std::vector<UUID> boot_uuids_;
     std::vector<message_bridge::State> client_state_;
@@ -208,12 +109,12 @@
 
     std::vector<std::pair<const Channel *, DelayersVector *>> delayer_watchers_;
 
-    std::function<void()> fn_;
+    std::function<void(uint32_t, monotonic_clock::time_point)> fn_;
 
     NodeEventLoopFactory *node_factory_;
     std::unique_ptr<aos::EventLoop> event_loop;
     ChannelTimestampSender timestamp_loggers;
-    std::unique_ptr<MessageBridgeServerStatus> server_status;
+    std::unique_ptr<MessageBridgeServerStatus> server_status_;
     std::unique_ptr<MessageBridgeClientStatus> client_status;
 
     // List of delayers to update whenever this node starts or stops.
diff --git a/aos/flatbuffer_introspection.cc b/aos/flatbuffer_introspection.cc
index 4dd7f2a..163c281 100644
--- a/aos/flatbuffer_introspection.cc
+++ b/aos/flatbuffer_introspection.cc
@@ -61,7 +61,7 @@
 void FloatToString(double val, reflection::BaseType type,
                    FastStringBuilder *out) {
   if (std::isnan(val)) {
-    out->Append("null");
+    out->Append(std::signbit(val) ? "-nan" : "nan");
     return;
   }
   switch (type) {
@@ -228,6 +228,9 @@
         }
 
         out->AppendChar('[');
+        if (!wrap) {
+          out->AppendChar(' ');
+        }
         for (flatbuffers::uoffset_t i = 0; i < vector->size(); ++i) {
           if (i != 0) {
             if (wrap) {
@@ -271,6 +274,8 @@
         }
         if (wrap) {
           AddWrapping(out, tree_depth);
+        } else {
+          out->AppendChar(' ');
         }
         out->AppendChar(']');
       } else {
@@ -327,6 +332,9 @@
   }
 
   out->AppendChar('{');
+  if (!wrap) {
+    out->AppendChar(' ');
+  }
   for (const reflection::Field *field : *obj->fields()) {
     // Check whether this object has the field populated (even for structs,
     // which should have all fields populated)
@@ -355,6 +363,8 @@
 
   if (wrap) {
     AddWrapping(out, tree_depth);
+  } else {
+    out->AppendChar(' ');
   }
 
   out->AppendChar('}');
diff --git a/aos/flatbuffer_introspection_test.cc b/aos/flatbuffer_introspection_test.cc
index cc7d627..54c6f18 100644
--- a/aos/flatbuffer_introspection_test.cc
+++ b/aos/flatbuffer_introspection_test.cc
@@ -45,9 +45,9 @@
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
   EXPECT_EQ(out,
-            "{\"foo_bool\": true, \"foo_byte\": -5, \"foo_int\": -20, "
+            "{ \"foo_bool\": true, \"foo_byte\": -5, \"foo_int\": -20, "
             "\"foo_long\": -100, \"foo_short\": -10, \"foo_ubyte\": 5, "
-            "\"foo_uint\": 20, \"foo_ulong\": 100, \"foo_ushort\": 10}");
+            "\"foo_uint\": 20, \"foo_ulong\": 100, \"foo_ushort\": 10 }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, FloatTest) {
@@ -62,7 +62,7 @@
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
   EXPECT_EQ(out,
-            "{\"foo_double\": 0.555555555555556, \"foo_float\": 0.333333}");
+            "{ \"foo_double\": 0.555555555555556, \"foo_float\": 0.333333 }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, NanFloatTest) {
@@ -76,7 +76,7 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(out, "{\"foo_double\": null, \"foo_float\": null}");
+  EXPECT_EQ(out, "{ \"foo_double\": nan, \"foo_float\": nan }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, VectorScalarTest) {
@@ -129,18 +129,18 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(
-      out,
-      "{\"vector_foo_bool\": [true, false, true, false], \"vector_foo_byte\": "
-      "[-3, -2, -1, 0, 1, 2, 3], \"vector_foo_double\": [0, 0.111111111111111, "
-      "0.222222222222222, 0.333333333333333], \"vector_foo_float\": [0, "
-      "0.111111, 0.222222, 0.333333], \"vector_foo_int\": [-300, -200, -100, "
-      "0, 100, 200, 300], \"vector_foo_long\": [-3000, -2000, -1000, 0, 1000, "
-      "2000, 3000], \"vector_foo_short\": [-30, -20, -10, 0, 10, 20, 30], "
-      "\"vector_foo_ubyte\": [0, 1, 2, 3, 4, 5, 6], \"vector_foo_uint\": [0, "
-      "100, 200, 300, 400, 500, 600], \"vector_foo_ulong\": [0, 1000, 2000, "
-      "3000, 4000, 5000, 6000], \"vector_foo_ushort\": [0, 10, 20, 30, 40, 50, "
-      "60]}");
+  EXPECT_EQ(out,
+            "{ \"vector_foo_bool\": [ true, false, true, false ], "
+            "\"vector_foo_byte\": [ -3, -2, -1, 0, 1, 2, 3 ], "
+            "\"vector_foo_double\": [ 0, 0.111111111111111, 0.222222222222222, "
+            "0.333333333333333 ], \"vector_foo_float\": [ 0, 0.111111, "
+            "0.222222, 0.333333 ], \"vector_foo_int\": [ -300, -200, -100, 0, "
+            "100, 200, 300 ], \"vector_foo_long\": [ -3000, -2000, -1000, 0, "
+            "1000, 2000, 3000 ], \"vector_foo_short\": [ -30, -20, -10, 0, 10, "
+            "20, 30 ], \"vector_foo_ubyte\": [ 0, 1, 2, 3, 4, 5, 6 ], "
+            "\"vector_foo_uint\": [ 0, 100, 200, 300, 400, 500, 600 ], "
+            "\"vector_foo_ulong\": [ 0, 1000, 2000, 3000, 4000, 5000, 6000 ], "
+            "\"vector_foo_ushort\": [ 0, 10, 20, 30, 40, 50, 60 ] }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, StringTest) {
@@ -155,7 +155,7 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(out, "{\"foo_string\": \"I <3 FlatBuffers!\"}");
+  EXPECT_EQ(out, "{ \"foo_string\": \"I <3 FlatBuffers!\" }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, EnumTest) {
@@ -168,7 +168,7 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(out, "{\"foo_enum\": \"UShort\"}");
+  EXPECT_EQ(out, "{ \"foo_enum\": \"UShort\" }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, EnumWithUnknownValueTest) {
@@ -183,7 +183,7 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(out, "{\"foo_enum\": 123}");
+  EXPECT_EQ(out, "{ \"foo_enum\": 123 }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, VectorStringTest) {
@@ -221,10 +221,11 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(out,
-            "{\"vector_foo_string\": [\"abc\", \"acb\"], \"vov\": {\"v\": "
-            "[{\"str\": [\"abc\", \"acb\"]}, {\"str\": [\"bac\", \"bca\"]}, "
-            "{\"str\": [\"cab\", \"cba\"]}]}}");
+  EXPECT_EQ(
+      out,
+      "{ \"vector_foo_string\": [ \"abc\", \"acb\" ], \"vov\": { \"v\": "
+      "[ { \"str\": [ \"abc\", \"acb\" ] }, { \"str\": [ \"bac\", \"bca\" ] }, "
+      "{ \"str\": [ \"cab\", \"cba\" ] } ] } }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, TableTest) {
@@ -254,10 +255,10 @@
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
   EXPECT_EQ(out,
-            "{\"foo_byte\": 5, \"foo_string\": \"Root Config String\", "
-            "\"nested_config\": {\"foo_byte\": 10, \"foo_string\": \"Nested "
-            "Config String\", \"vector_foo_byte\": [6, 7, 8, 9, 10]}, "
-            "\"vector_foo_byte\": [0, 1, 2, 3, 4, 5]}");
+            "{ \"foo_byte\": 5, \"foo_string\": \"Root Config String\", "
+            "\"nested_config\": { \"foo_byte\": 10, \"foo_string\": \"Nested "
+            "Config String\", \"vector_foo_byte\": [ 6, 7, 8, 9, 10 ] }, "
+            "\"vector_foo_byte\": [ 0, 1, 2, 3, 4, 5 ] }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, StructTest) {
@@ -275,8 +276,8 @@
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
   EXPECT_EQ(out,
-            "{\"foo_struct\": {\"foo_byte\": 5, \"nested_struct\": "
-            "{\"foo_byte\": 10}}}");
+            "{ \"foo_struct\": { \"foo_byte\": 5, \"nested_struct\": "
+            "{ \"foo_byte\": 10 } } }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, VectorStructTest) {
@@ -295,9 +296,9 @@
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
   EXPECT_EQ(out,
-            "{\"vector_foo_struct\": [{\"foo_byte\": 5, \"nested_struct\": "
-            "{\"foo_byte\": 1}}, {\"foo_byte\": 10, \"nested_struct\": "
-            "{\"foo_byte\": 1}}]}");
+            "{ \"vector_foo_struct\": [ { \"foo_byte\": 5, \"nested_struct\": "
+            "{ \"foo_byte\": 1 } }, { \"foo_byte\": 10, \"nested_struct\": "
+            "{ \"foo_byte\": 1 } } ] }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, VectorEnumTest) {
@@ -313,7 +314,7 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(out, "{\"vector_foo_enum\": [\"UShort\", \"Obj\", \"UInt\"]}");
+  EXPECT_EQ(out, "{ \"vector_foo_enum\": [ \"UShort\", \"Obj\", \"UInt\" ] }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, StructEnumTest) {
@@ -328,7 +329,7 @@
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
 
-  EXPECT_EQ(out, "{\"foo_struct_enum\": {\"foo_enum\": \"UShort\"}}");
+  EXPECT_EQ(out, "{ \"foo_struct_enum\": { \"foo_enum\": \"UShort\" } }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, StringEscapeTest) {
@@ -342,7 +343,7 @@
   builder.Finish(config_builder.Finish());
 
   std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
-  EXPECT_EQ(out, "{\"foo_string\": \"\\\"\\\\\\b\\f\\n\\r\\t\"}");
+  EXPECT_EQ(out, "{ \"foo_string\": \"\\\"\\\\\\b\\f\\n\\r\\t\" }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, TrimmedVector) {
@@ -362,7 +363,7 @@
   std::string out =
       FlatbufferToJson(schema_, builder.GetBufferPointer(),
                        {.multi_line = false, .max_vector_size = 100});
-  EXPECT_EQ(out, "{\"vector_foo_int\": [ \"... 101 elements ...\" ]}");
+  EXPECT_EQ(out, "{ \"vector_foo_int\": [ \"... 101 elements ...\" ] }");
 }
 
 TEST_F(FlatbufferIntrospectionTest, MultilineTest) {
@@ -402,7 +403,7 @@
             "{\n"
             "  \"foo_struct\": {\n"
             "    \"foo_byte\": 5,\n"
-            "    \"nested_struct\": {\"foo_byte\": 10}\n"
+            "    \"nested_struct\": { \"foo_byte\": 10 }\n"
             "  }\n"
             "}");
 }
@@ -428,11 +429,11 @@
             "  \"vector_foo_struct\": [\n"
             "    {\n"
             "      \"foo_byte\": 5,\n"
-            "      \"nested_struct\": {\"foo_byte\": 1}\n"
+            "      \"nested_struct\": { \"foo_byte\": 1 }\n"
             "    },\n"
             "    {\n"
             "      \"foo_byte\": 10,\n"
-            "      \"nested_struct\": {\"foo_byte\": 1}\n"
+            "      \"nested_struct\": { \"foo_byte\": 1 }\n"
             "    }\n"
             "  ]\n"
             "}");
@@ -464,10 +465,10 @@
 
   EXPECT_EQ(out,
             "{\n"
-            "  \"vector_foo_double\": [0, 0.111111111111111, "
-            "0.222222222222222, 0.333333333333333],\n"
-            "  \"vector_foo_float\": [0, 0.111111, 0.222222, 0.333333],\n"
-            "  \"vector_foo_int\": [-300, -200, -100, 0, 100, 200, 300]\n"
+            "  \"vector_foo_double\": [ 0, 0.111111111111111, "
+            "0.222222222222222, 0.333333333333333 ],\n"
+            "  \"vector_foo_float\": [ 0, 0.111111, 0.222222, 0.333333 ],\n"
+            "  \"vector_foo_int\": [ -300, -200, -100, 0, 100, 200, 300 ]\n"
             "}");
 }
 
diff --git a/aos/flatbuffers/base.cc b/aos/flatbuffers/base.cc
index 697f837..8ad3b98 100644
--- a/aos/flatbuffers/base.cc
+++ b/aos/flatbuffers/base.cc
@@ -7,6 +7,31 @@
 }
 }  // namespace
 
+ResizeableObject::ResizeableObject(ResizeableObject &&other)
+    : buffer_(other.buffer_),
+      parent_(other.parent_),
+      owned_allocator_(std::move(other.owned_allocator_)),
+      allocator_(other.allocator_) {
+  // At this stage in the move the move constructors of the inherited types have
+  // not yet been called, so we edit the state of the other object now so that
+  // when everything is moved over into the new objects they will have the
+  // correct pointers.
+  for (size_t index = 0; index < other.NumberOfSubObjects(); ++index) {
+    SubObject object = other.GetSubObject(index);
+    if (object.object != nullptr) {
+      object.object->parent_ = this;
+    }
+  }
+  other.buffer_ = {};
+  other.allocator_ = nullptr;
+  other.parent_ = nullptr;
+  // Sanity check that the std::unique_ptr move didn't reallocate/move memory
+  // around.
+  if (owned_allocator_.get() != nullptr) {
+    CHECK_EQ(owned_allocator_.get(), allocator_);
+  }
+}
+
 bool ResizeableObject::InsertBytes(void *insertion_point, size_t bytes,
                                    SetZero set_zero) {
   // See comments on InsertBytes() declaration and in FixObjects()
diff --git a/aos/flatbuffers/base.h b/aos/flatbuffers/base.h
index f99dbb8..387dbc3 100644
--- a/aos/flatbuffers/base.h
+++ b/aos/flatbuffers/base.h
@@ -99,13 +99,7 @@
   // Users do not end up using the move constructor; however, it is needed to
   // handle the fact that a ResizeableObject may be a member of an std::vector
   // in the various generated types.
-  ResizeableObject(ResizeableObject &&other)
-      : buffer_(other.buffer_),
-        owned_allocator_(std::move(other.owned_allocator_)),
-        allocator_(owned_allocator_.get()) {
-    other.buffer_ = {};
-    other.allocator_ = nullptr;
-  }
+  ResizeableObject(ResizeableObject &&other);
   // Required alignment of this object.
   virtual size_t Alignment() const = 0;
   // Offset from the start of buffer() to the actual start of the object in
diff --git a/aos/flatbuffers/static_flatbuffers_test.cc b/aos/flatbuffers/static_flatbuffers_test.cc
index 52fa01e..1846540 100644
--- a/aos/flatbuffers/static_flatbuffers_test.cc
+++ b/aos/flatbuffers/static_flatbuffers_test.cc
@@ -1094,4 +1094,58 @@
   VerifyJson<::aos::testing::UseSchemaStatic>("{\n\n}");
 }
 
+// Tests that we can use the move constructor on a Builder.
+TEST_F(StaticFlatbuffersTest, BuilderMoveConstructor) {
+  uint8_t buffer[Builder<TestTableStatic>::kBufferSize];
+  aos::fbs::SpanAllocator allocator({buffer, sizeof(buffer)});
+  Builder<TestTableStatic> builder_from(&allocator);
+  Builder<TestTableStatic> builder(std::move(builder_from));
+  TestTableStatic *object = builder.get();
+  object->set_scalar(123);
+  {
+    auto vector = object->add_vector_of_scalars();
+    ASSERT_TRUE(vector->emplace_back(4));
+    ASSERT_TRUE(vector->emplace_back(5));
+  }
+  {
+    auto string = object->add_string();
+    string->SetString("Hello, World!");
+  }
+  {
+    auto vector_of_strings = object->add_vector_of_strings();
+    auto sub_string = CHECK_NOTNULL(vector_of_strings->emplace_back());
+    ASSERT_TRUE(sub_string->emplace_back('D'));
+  }
+  { object->set_substruct({971, 254}); }
+  {
+    auto subtable = object->add_subtable();
+    subtable->set_foo(1234);
+  }
+  {
+    auto vector = object->add_vector_of_structs();
+    ASSERT_TRUE(vector->emplace_back({48, 67}));
+    ASSERT_TRUE(vector->emplace_back({118, 148}));
+    ASSERT_TRUE(vector->emplace_back({971, 973}));
+    // Max vector size is three; this should fail.
+    ASSERT_FALSE(vector->emplace_back({1114, 2056}));
+    // We don't have any extra space available.
+    ASSERT_FALSE(vector->reserve(4));
+    ASSERT_FALSE(vector->emplace_back({1114, 2056}));
+  }
+  {
+    auto vector = object->add_vector_of_tables();
+    auto subobject = vector->emplace_back();
+    subobject->set_foo(222);
+  }
+  {
+    auto subtable = object->add_included_table();
+    subtable->set_foo(included::TestEnum::B);
+  }
+  ASSERT_TRUE(builder.AsFlatbufferSpan().Verify());
+  VLOG(1) << aos::FlatbufferToJson(builder.AsFlatbufferSpan(),
+                                   {.multi_line = true});
+  VLOG(1) << AnnotateBinaries(test_schema_, builder.buffer());
+  TestMemory(builder.buffer());
+}
+
 }  // namespace aos::fbs::testing
diff --git a/aos/flatbuffers/static_vector.h b/aos/flatbuffers/static_vector.h
index 6133075..4ce49a7 100644
--- a/aos/flatbuffers/static_vector.h
+++ b/aos/flatbuffers/static_vector.h
@@ -291,6 +291,9 @@
   // if the allocation failed for some reason.
   // Note that reductions in size will not currently result in the allocated
   // size actually changing.
+  // For vectors of non-inline types (e.g., vectors of strings or vectors of
+  // tables), reserve() will allocate memory in an internal vector that we use
+  // for storing some metadata.
   [[nodiscard]] bool reserve(size_t new_length) {
     if (new_length > allocated_length_) {
       const size_t new_elements = new_length - allocated_length_;
diff --git a/aos/ipc_lib/lockless_queue.cc b/aos/ipc_lib/lockless_queue.cc
index 9d14d8f..57a2e9e 100644
--- a/aos/ipc_lib/lockless_queue.cc
+++ b/aos/ipc_lib/lockless_queue.cc
@@ -953,6 +953,7 @@
     const char *data, size_t length,
     monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
+    monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid,
     monotonic_clock::time_point *monotonic_sent_time,
     realtime_clock::time_point *realtime_sent_time, uint32_t *queue_index) {
@@ -962,13 +963,15 @@
   // adhere to this convention and place it at the end.
   memcpy((reinterpret_cast<char *>(Data()) + size() - length), data, length);
   return Send(length, monotonic_remote_time, realtime_remote_time,
-              remote_queue_index, source_boot_uuid, monotonic_sent_time,
-              realtime_sent_time, queue_index);
+              monotonic_remote_transmit_time, remote_queue_index,
+              source_boot_uuid, monotonic_sent_time, realtime_sent_time,
+              queue_index);
 }
 
 LocklessQueueSender::Result LocklessQueueSender::Send(
     size_t length, monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
+    monotonic_clock::time_point monotonic_remote_transmit_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid,
     monotonic_clock::time_point *monotonic_sent_time,
     realtime_clock::time_point *realtime_sent_time, uint32_t *queue_index) {
@@ -997,6 +1000,8 @@
   message->header.source_boot_uuid = source_boot_uuid;
   message->header.monotonic_remote_time = monotonic_remote_time;
   message->header.realtime_remote_time = realtime_remote_time;
+  message->header.monotonic_remote_transmit_time =
+      monotonic_remote_transmit_time;
 
   Index to_replace = Index::Invalid();
   while (true) {
@@ -1298,6 +1303,7 @@
     monotonic_clock::time_point *monotonic_sent_time,
     realtime_clock::time_point *realtime_sent_time,
     monotonic_clock::time_point *monotonic_remote_time,
+    monotonic_clock::time_point *monotonic_remote_transmit_time,
     realtime_clock::time_point *realtime_remote_time,
     uint32_t *remote_queue_index, UUID *source_boot_uuid, size_t *length,
     char *data,
@@ -1379,6 +1385,8 @@
   context.monotonic_event_time = m->header.monotonic_sent_time;
   context.realtime_event_time = m->header.realtime_sent_time;
   context.monotonic_remote_time = m->header.monotonic_remote_time;
+  context.monotonic_remote_transmit_time =
+      m->header.monotonic_remote_transmit_time;
   context.realtime_remote_time = m->header.realtime_remote_time;
   context.queue_index = queue_index.index();
   if (m->header.remote_queue_index == 0xffffffffu) {
@@ -1443,6 +1451,7 @@
   *realtime_sent_time = context.realtime_event_time;
   *remote_queue_index = context.remote_queue_index;
   *monotonic_remote_time = context.monotonic_remote_time;
+  *monotonic_remote_transmit_time = context.monotonic_remote_transmit_time;
   *realtime_remote_time = context.realtime_remote_time;
   *source_boot_uuid = context.source_boot_uuid;
   *length = context.size;
@@ -1597,6 +1606,12 @@
         << m->header.monotonic_remote_time << " 0x" << std::hex
         << m->header.monotonic_remote_time.time_since_epoch().count()
         << std::dec << ::std::endl;
+    ::std::cout
+        << "        monotonic_clock::time_point "
+           "monotonic_remote_transmit_time = "
+        << m->header.monotonic_remote_transmit_time << " 0x" << std::hex
+        << m->header.monotonic_remote_transmit_time.time_since_epoch().count()
+        << std::dec << ::std::endl;
     ::std::cout << "        realtime_clock::time_point realtime_remote_time = "
                 << m->header.realtime_remote_time << " 0x" << std::hex
                 << m->header.realtime_remote_time.time_since_epoch().count()
diff --git a/aos/ipc_lib/lockless_queue.h b/aos/ipc_lib/lockless_queue.h
index 4f867b9..462b2b6 100644
--- a/aos/ipc_lib/lockless_queue.h
+++ b/aos/ipc_lib/lockless_queue.h
@@ -91,6 +91,7 @@
     // passed through.
     monotonic_clock::time_point monotonic_remote_time;
     realtime_clock::time_point realtime_remote_time;
+    monotonic_clock::time_point monotonic_remote_transmit_time;
 
     // Queue index from the remote node.
     uint32_t remote_queue_index;
@@ -326,6 +327,7 @@
   LocklessQueueSender::Result Send(
       size_t length, monotonic_clock::time_point monotonic_remote_time,
       realtime_clock::time_point realtime_remote_time,
+      monotonic_clock::time_point monotonic_remote_transmit_time,
       uint32_t remote_queue_index, const UUID &source_boot_uuid,
       monotonic_clock::time_point *monotonic_sent_time = nullptr,
       realtime_clock::time_point *realtime_sent_time = nullptr,
@@ -336,6 +338,7 @@
       const char *data, size_t length,
       monotonic_clock::time_point monotonic_remote_time,
       realtime_clock::time_point realtime_remote_time,
+      monotonic_clock::time_point monotonic_remote_transmit_time,
       uint32_t remote_queue_index, const UUID &source_boot_uuid,
       monotonic_clock::time_point *monotonic_sent_time = nullptr,
       realtime_clock::time_point *realtime_sent_time = nullptr,
@@ -442,6 +445,7 @@
       uint32_t queue_index, monotonic_clock::time_point *monotonic_sent_time,
       realtime_clock::time_point *realtime_sent_time,
       monotonic_clock::time_point *monotonic_remote_time,
+      monotonic_clock::time_point *monotonic_remote_transmit_time,
       realtime_clock::time_point *realtime_remote_time,
       uint32_t *remote_queue_index, UUID *source_boot_uuid, size_t *length,
       char *data,
diff --git a/aos/ipc_lib/lockless_queue_death_test.cc b/aos/ipc_lib/lockless_queue_death_test.cc
index ccf95f7..bc5ebb7 100644
--- a/aos/ipc_lib/lockless_queue_death_test.cc
+++ b/aos/ipc_lib/lockless_queue_death_test.cc
@@ -79,10 +79,11 @@
         for (int i = 0; i < 5; ++i) {
           char data[100];
           size_t s = snprintf(data, sizeof(data), "foobar%d", i + 1);
-          ASSERT_EQ(sender.Send(data, s + 1, monotonic_clock::min_time,
-                                realtime_clock::min_time, 0xffffffffl,
-                                UUID::Zero(), nullptr, nullptr, nullptr),
-                    LocklessQueueSender::Result::GOOD);
+          ASSERT_EQ(
+              sender.Send(data, s + 1, monotonic_clock::min_time,
+                          realtime_clock::min_time, monotonic_clock::min_time,
+                          0xffffffffl, UUID::Zero(), nullptr, nullptr, nullptr),
+              LocklessQueueSender::Result::GOOD);
           // Pin a message, so when we keep writing we will exercise the pinning
           // logic.
           if (i == 1) {
@@ -156,10 +157,11 @@
           // Send a message to make sure that the queue still works.
           char data[100];
           size_t s = snprintf(data, sizeof(data), "foobar%d", 971);
-          ASSERT_EQ(sender.Send(data, s + 1, monotonic_clock::min_time,
-                                realtime_clock::min_time, 0xffffffffl,
-                                UUID::Zero(), nullptr, nullptr, nullptr),
-                    LocklessQueueSender::Result::GOOD);
+          ASSERT_EQ(
+              sender.Send(data, s + 1, monotonic_clock::min_time,
+                          realtime_clock::min_time, monotonic_clock::min_time,
+                          0xffffffffl, UUID::Zero(), nullptr, nullptr, nullptr),
+              LocklessQueueSender::Result::GOOD);
         }
 
         // Now loop through the queue and make sure the number in the snprintf
@@ -175,17 +177,18 @@
           monotonic_clock::time_point monotonic_sent_time;
           realtime_clock::time_point realtime_sent_time;
           monotonic_clock::time_point monotonic_remote_time;
+          monotonic_clock::time_point monotonic_remote_transmit_time;
           realtime_clock::time_point realtime_remote_time;
           uint32_t remote_queue_index;
           UUID source_boot_uuid;
           char read_data[1024];
           size_t length;
 
-          LocklessQueueReader::Result read_result =
-              reader.Read(i, &monotonic_sent_time, &realtime_sent_time,
-                          &monotonic_remote_time, &realtime_remote_time,
-                          &remote_queue_index, &source_boot_uuid, &length,
-                          &(read_data[0]), std::ref(should_read));
+          LocklessQueueReader::Result read_result = reader.Read(
+              i, &monotonic_sent_time, &realtime_sent_time,
+              &monotonic_remote_time, &monotonic_remote_transmit_time,
+              &realtime_remote_time, &remote_queue_index, &source_boot_uuid,
+              &length, &(read_data[0]), std::ref(should_read));
 
           if (read_result != LocklessQueueReader::Result::GOOD) {
             if (read_result == LocklessQueueReader::Result::TOO_OLD) {
diff --git a/aos/ipc_lib/lockless_queue_test.cc b/aos/ipc_lib/lockless_queue_test.cc
index bfd9916..a2c0992 100644
--- a/aos/ipc_lib/lockless_queue_test.cc
+++ b/aos/ipc_lib/lockless_queue_test.cc
@@ -251,8 +251,8 @@
     char data[100];
     size_t s = snprintf(data, sizeof(data), "foobar%d", i);
     ASSERT_EQ(sender.Send(data, s, monotonic_clock::min_time,
-                          realtime_clock::min_time, 0xffffffffu, UUID::Zero(),
-                          nullptr, nullptr, nullptr),
+                          realtime_clock::min_time, monotonic_clock::min_time,
+                          0xffffffffu, UUID::Zero(), nullptr, nullptr, nullptr),
               LocklessQueueSender::Result::GOOD);
 
     // Confirm that the queue index still makes sense.  This is easier since the
@@ -263,6 +263,7 @@
     monotonic_clock::time_point monotonic_sent_time;
     realtime_clock::time_point realtime_sent_time;
     monotonic_clock::time_point monotonic_remote_time;
+    monotonic_clock::time_point monotonic_remote_transmit_time;
     realtime_clock::time_point realtime_remote_time;
     uint32_t remote_queue_index;
     UUID source_boot_uuid;
@@ -277,8 +278,9 @@
     }
     LocklessQueueReader::Result read_result = reader.Read(
         index.index(), &monotonic_sent_time, &realtime_sent_time,
-        &monotonic_remote_time, &realtime_remote_time, &remote_queue_index,
-        &source_boot_uuid, &length, &(read_data[0]), std::ref(should_read));
+        &monotonic_remote_time, &monotonic_remote_transmit_time,
+        &realtime_remote_time, &remote_queue_index, &source_boot_uuid, &length,
+        &(read_data[0]), std::ref(should_read));
 
     // This should either return GOOD, or TOO_OLD if it is before the start of
     // the queue.
@@ -450,6 +452,7 @@
     monotonic_clock::time_point monotonic_sent_time;
     realtime_clock::time_point realtime_sent_time;
     monotonic_clock::time_point monotonic_remote_time;
+    monotonic_clock::time_point monotonic_remote_transmit_time;
     realtime_clock::time_point realtime_remote_time;
     uint32_t remote_queue_index;
     UUID source_boot_uuid;
@@ -458,8 +461,9 @@
 
     LocklessQueueReader::Result read_result = reader.Read(
         i, &monotonic_sent_time, &realtime_sent_time, &monotonic_remote_time,
-        &realtime_remote_time, &remote_queue_index, &source_boot_uuid, &length,
-        &(read_data[0]), should_read_callback);
+        &monotonic_remote_transmit_time, &realtime_remote_time,
+        &remote_queue_index, &source_boot_uuid, &length, &(read_data[0]),
+        should_read_callback);
 
     if (read_result != LocklessQueueReader::Result::GOOD) {
       if (read_result == LocklessQueueReader::Result::TOO_OLD) {
@@ -526,10 +530,11 @@
         for (int i = 0; i < 5; ++i) {
           char data[100];
           size_t s = snprintf(data, sizeof(data), "foobar%d", i + 1);
-          ASSERT_EQ(sender.Send(data, s + 1, monotonic_clock::min_time,
-                                realtime_clock::min_time, 0xffffffffl,
-                                UUID::Zero(), nullptr, nullptr, nullptr),
-                    LocklessQueueSender::Result::GOOD);
+          ASSERT_EQ(
+              sender.Send(data, s + 1, monotonic_clock::min_time,
+                          realtime_clock::min_time, monotonic_clock::min_time,
+                          0xffffffffl, UUID::Zero(), nullptr, nullptr, nullptr),
+              LocklessQueueSender::Result::GOOD);
         }
       },
       [config, &tid](void *raw_memory) {
@@ -549,10 +554,11 @@
         {
           char data[100];
           size_t s = snprintf(data, sizeof(data), "foobar%d", i + 1);
-          ASSERT_EQ(sender.Send(data, s + 1, monotonic_clock::min_time,
-                                realtime_clock::min_time, 0xffffffffl,
-                                UUID::Zero(), nullptr, nullptr, nullptr),
-                    LocklessQueueSender::Result::GOOD);
+          ASSERT_EQ(
+              sender.Send(data, s + 1, monotonic_clock::min_time,
+                          realtime_clock::min_time, monotonic_clock::min_time,
+                          0xffffffffl, UUID::Zero(), nullptr, nullptr, nullptr),
+              LocklessQueueSender::Result::GOOD);
         }
 
         // Now, make sure we can send 1 message and receive it to confirm we
diff --git a/aos/ipc_lib/memory_mapped_queue.cc b/aos/ipc_lib/memory_mapped_queue.cc
index d73b850..dc457fe 100644
--- a/aos/ipc_lib/memory_mapped_queue.cc
+++ b/aos/ipc_lib/memory_mapped_queue.cc
@@ -16,15 +16,14 @@
 
 std::string ShmPath(std::string_view shm_base, const Channel *channel) {
   CHECK(channel->has_type());
-  return ShmFolder(shm_base, channel) + channel->type()->str() + ".v6";
+  return ShmFolder(shm_base, channel) + channel->type()->str() + ".v7";
 }
 
-void PageFaultDataWrite(char *data, size_t size) {
+void PageFaultDataWrite(char *data, size_t size, const long page_size) {
   // This just has to divide the actual page size. Being smaller will make this
   // a bit slower than necessary, but not much. 1024 is a pretty conservative
   // choice (most pages are probably 4096).
-  static constexpr size_t kPageSize = 1024;
-  const size_t pages = (size + kPageSize - 1) / kPageSize;
+  const size_t pages = (size + page_size - 1) / page_size;
   for (size_t i = 0; i < pages; ++i) {
     char zero = 0;
     // We need to ensure there's a writable pagetable entry, but avoid modifying
@@ -39,20 +38,16 @@
     //
     // This is the simplest operation I could think of which achieves that:
     // "store 0 if it's already 0".
-    __atomic_compare_exchange_n(&data[i * kPageSize], &zero, 0, true,
+    __atomic_compare_exchange_n(&data[i * page_size], &zero, 0, true,
                                 __ATOMIC_RELAXED, __ATOMIC_RELAXED);
   }
 }
 
-void PageFaultDataRead(const char *data, size_t size) {
-  // This just has to divide the actual page size. Being smaller will make this
-  // a bit slower than necessary, but not much. 1024 is a pretty conservative
-  // choice (most pages are probably 4096).
-  static constexpr size_t kPageSize = 1024;
-  const size_t pages = (size + kPageSize - 1) / kPageSize;
+void PageFaultDataRead(const char *data, size_t size, const long page_size) {
+  const size_t pages = (size + page_size - 1) / page_size;
   for (size_t i = 0; i < pages; ++i) {
     // We need to ensure there's a readable pagetable entry.
-    __atomic_load_n(&data[i * kPageSize], __ATOMIC_RELAXED);
+    __atomic_load_n(&data[i * page_size], __ATOMIC_RELAXED);
   }
 }
 
@@ -85,6 +80,7 @@
                                      const Configuration *config,
                                      const Channel *channel)
     : config_(MakeQueueConfiguration(config, channel)) {
+  const long kSystemPageSize = sysconf(_SC_PAGESIZE);
   std::string path = ShmPath(shm_base, channel);
 
   size_ = ipc_lib::LocklessQueueMemorySize(config_);
@@ -128,8 +124,9 @@
   const_data_ = mmap(NULL, size_, PROT_READ, MAP_SHARED, fd, 0);
   PCHECK(const_data_ != MAP_FAILED);
   PCHECK(close(fd) == 0);
-  PageFaultDataWrite(static_cast<char *>(data_), size_);
-  PageFaultDataRead(static_cast<const char *>(const_data_), size_);
+  PageFaultDataWrite(static_cast<char *>(data_), size_, kSystemPageSize);
+  PageFaultDataRead(static_cast<const char *>(const_data_), size_,
+                    kSystemPageSize);
 
   ipc_lib::InitializeLocklessQueueMemory(memory(), config_);
 }
diff --git a/aos/ipc_lib/queue_racer.cc b/aos/ipc_lib/queue_racer.cc
index aa73f2b..2797c24 100644
--- a/aos/ipc_lib/queue_racer.cc
+++ b/aos/ipc_lib/queue_racer.cc
@@ -197,7 +197,8 @@
         ++started_writes_;
         auto result =
             sender.Send(sizeof(ThreadPlusCount), aos::monotonic_clock::min_time,
-                        aos::realtime_clock::min_time, 0xffffffff,
+                        aos::realtime_clock::min_time,
+                        aos::monotonic_clock::min_time, 0xffffffff,
                         UUID::FromSpan(absl::Span<const uint8_t>(
                             reinterpret_cast<const uint8_t *>(&tpc),
                             sizeof(ThreadPlusCount))),
@@ -309,6 +310,7 @@
     monotonic_clock::time_point monotonic_sent_time;
     realtime_clock::time_point realtime_sent_time;
     monotonic_clock::time_point monotonic_remote_time;
+    monotonic_clock::time_point monotonic_remote_transmit_time;
     realtime_clock::time_point realtime_remote_time;
     UUID source_boot_uuid;
     uint32_t remote_queue_index;
@@ -321,14 +323,16 @@
                 0xffffffffu, LocklessQueueSize(queue_.memory())));
     LocklessQueueReader::Result read_result =
         set_should_read
-            ? reader.Read(wrapped_i, &monotonic_sent_time, &realtime_sent_time,
-                          &monotonic_remote_time, &realtime_remote_time,
-                          &remote_queue_index, &source_boot_uuid, &length,
-                          &(read_data[0]), std::ref(should_read))
+            ? reader.Read(
+                  wrapped_i, &monotonic_sent_time, &realtime_sent_time,
+                  &monotonic_remote_time, &monotonic_remote_transmit_time,
+                  &realtime_remote_time, &remote_queue_index, &source_boot_uuid,
+                  &length, &(read_data[0]), std::ref(should_read))
             : reader.Read(wrapped_i, &monotonic_sent_time, &realtime_sent_time,
-                          &monotonic_remote_time, &realtime_remote_time,
-                          &remote_queue_index, &source_boot_uuid, &length,
-                          &(read_data[0]), nop);
+                          &monotonic_remote_time,
+                          &monotonic_remote_transmit_time,
+                          &realtime_remote_time, &remote_queue_index,
+                          &source_boot_uuid, &length, &(read_data[0]), nop);
 
     // The code in lockless_queue.cc reads everything but data, checks that the
     // header hasn't changed, then reads the data.  So, if we succeed and both
diff --git a/aos/json_to_flatbuffer_test.cc b/aos/json_to_flatbuffer_test.cc
index 85e21d4..7c69acd 100644
--- a/aos/json_to_flatbuffer_test.cc
+++ b/aos/json_to_flatbuffer_test.cc
@@ -11,6 +11,8 @@
 
 class JsonToFlatbufferTest : public ::testing::Test {
  public:
+  enum class TestReflection { kYes, kNo };
+
   JsonToFlatbufferTest() {}
 
   FlatbufferVector<reflection::Schema> Schema() {
@@ -18,14 +20,23 @@
         ArtifactPath("aos/json_to_flatbuffer.bfbs"));
   }
 
-  bool JsonAndBack(const ::std::string str) { return JsonAndBack(str, str); }
+  // JsonAndBack tests using both the reflection::Schema* as well as the
+  // minireflect tables for both parsing and outputting JSON. However, there are
+  // currently minor discrepencies between how the JSON output works for the two
+  // modes, so some tests must manually disable testing of the
+  // FlatbufferToJson() overload that takes a reflection::Schema*.
+  bool JsonAndBack(const char *str, TestReflection test_reflection_to_json =
+                                        TestReflection::kYes) {
+    return JsonAndBack(str, str, test_reflection_to_json);
+  }
 
-  bool JsonAndBack(const ::std::string in, const ::std::string out) {
-    printf("Testing: %s\n", in.c_str());
+  bool JsonAndBack(
+      const char *in, const char *out,
+      TestReflection test_reflection_to_json = TestReflection::kYes) {
     FlatbufferDetachedBuffer<Configuration> fb_typetable =
-        JsonToFlatbuffer<Configuration>(in.data());
+        JsonToFlatbuffer<Configuration>(in);
     FlatbufferDetachedBuffer<Configuration> fb_reflection =
-        JsonToFlatbuffer(in.data(), FlatbufferType(&Schema().message()));
+        JsonToFlatbuffer(in, FlatbufferType(&Schema().message()));
 
     if (fb_typetable.span().size() == 0) {
       return false;
@@ -36,13 +47,24 @@
 
     const ::std::string back_typetable = FlatbufferToJson(fb_typetable);
     const ::std::string back_reflection = FlatbufferToJson(fb_reflection);
+    const ::std::string back_reflection_reflection =
+        FlatbufferToJson(&Schema().message(), fb_reflection.span().data());
 
-    printf("Back to string via TypeTable: %s\n", back_typetable.c_str());
-    printf("Back to string via reflection: %s\n", back_reflection.c_str());
+    printf("Back to table via TypeTable and to string via TypeTable: %s\n",
+           back_typetable.c_str());
+    printf("Back to table via reflection and to string via TypeTable: %s\n",
+           back_reflection.c_str());
+    if (test_reflection_to_json == TestReflection::kYes) {
+      printf("Back to table via reflection and to string via reflection: %s\n",
+             back_reflection_reflection.c_str());
+    }
 
-    const bool as_expected = back_typetable == out && back_reflection == out;
+    const bool as_expected =
+        back_typetable == out && back_reflection == out &&
+        ((test_reflection_to_json == TestReflection::kNo) ||
+         (back_reflection_reflection == out));
     if (!as_expected) {
-      printf("But expected: %s\n", out.c_str());
+      printf("But expected: %s\n", out);
     }
     return as_expected;
   }
@@ -71,8 +93,10 @@
   EXPECT_TRUE(JsonAndBack("{ \"foo_long\": 5 }"));
   EXPECT_TRUE(JsonAndBack("{ \"foo_ulong\": 5 }"));
 
-  EXPECT_TRUE(JsonAndBack("{ \"foo_float\": 5.0 }"));
-  EXPECT_TRUE(JsonAndBack("{ \"foo_double\": 5.0 }"));
+  // TODO(james): Make FlatbufferToJson() always print out integer
+  // floating-point numbers identically.
+  EXPECT_TRUE(JsonAndBack("{ \"foo_float\": 5.0 }", TestReflection::kNo));
+  EXPECT_TRUE(JsonAndBack("{ \"foo_double\": 5.0 }", TestReflection::kNo));
 
   EXPECT_TRUE(JsonAndBack("{ \"foo_enum\": \"None\" }"));
   EXPECT_TRUE(JsonAndBack("{ \"foo_enum\": \"UType\" }"));
@@ -93,7 +117,8 @@
   EXPECT_TRUE(JsonAndBack(
       "{ \"foo_struct_scalars\": { \"foo_float\": 1.234, \"foo_double\": "
       "4.567, \"foo_int32\": -971, \"foo_uint32\": 4294967294, \"foo_int64\": "
-      "-1030, \"foo_uint64\": 18446744073709551614 } }"));
+      "-1030, \"foo_uint64\": 18446744073709551614 } }",
+      TestReflection::kNo));
   // Confirm that we parse integers into floating point fields correctly.
   EXPECT_TRUE(JsonAndBack(
       "{ \"foo_struct_scalars\": { \"foo_float\": 1, \"foo_double\": "
@@ -101,13 +126,15 @@
       "5, \"foo_uint64\": 6 } }",
       "{ \"foo_struct_scalars\": { \"foo_float\": 1.0, \"foo_double\": "
       "2.0, \"foo_int32\": 3, \"foo_uint32\": 4, \"foo_int64\": "
-      "5, \"foo_uint64\": 6 } }"));
+      "5, \"foo_uint64\": 6 } }",
+      TestReflection::kNo));
   EXPECT_TRUE(JsonAndBack(
       "{ \"vector_foo_struct_scalars\": [ { \"foo_float\": 1.234, "
       "\"foo_double\": 4.567, \"foo_int32\": -971, \"foo_uint32\": 4294967294, "
       "\"foo_int64\": -1030, \"foo_uint64\": 18446744073709551614 }, { "
       "\"foo_float\": 2.0, \"foo_double\": 4.1, \"foo_int32\": 10, "
-      "\"foo_uint32\": 13, \"foo_int64\": 15, \"foo_uint64\": 18 } ] }"));
+      "\"foo_uint32\": 13, \"foo_int64\": 15, \"foo_uint64\": 18 } ] }",
+      TestReflection::kNo));
   EXPECT_TRUE(
       JsonAndBack("{ \"foo_struct_enum\": { \"foo_enum\": \"UByte\" } }"));
   EXPECT_TRUE(
@@ -160,14 +187,24 @@
 
 // Tests that unicode is handled correctly
 TEST_F(JsonToFlatbufferTest, Unicode) {
-  EXPECT_TRUE(JsonAndBack("{ \"foo_string\": \"\\uF672\" }"));
-  EXPECT_TRUE(JsonAndBack("{ \"foo_string\": \"\\uEFEF\" }"));
-  EXPECT_TRUE(JsonAndBack("{ \"foo_string\": \"helloworld\\uD83E\\uDE94\" }"));
-  EXPECT_TRUE(JsonAndBack("{ \"foo_string\": \"\\uD83C\\uDF32\" }"));
-  EXPECT_FALSE(JsonAndBack("{ \"foo_string\": \"\\uP890\" }"));
-  EXPECT_FALSE(JsonAndBack("{ \"foo_string\": \"\\u!FA8\" }"));
-  EXPECT_FALSE(JsonAndBack("{ \"foo_string\": \"\\uF89\" }"));
-  EXPECT_FALSE(JsonAndBack("{ \"foo_string\": \"\\uD83C\" }"));
+  // The reflection-based FlatbufferToJson outputs actual unicode rather than
+  // escaped code-points.
+  EXPECT_TRUE(
+      JsonAndBack("{ \"foo_string\": \"\\uF672\" }", TestReflection::kNo));
+  EXPECT_TRUE(
+      JsonAndBack("{ \"foo_string\": \"\\uEFEF\" }", TestReflection::kNo));
+  EXPECT_TRUE(JsonAndBack("{ \"foo_string\": \"helloworld\\uD83E\\uDE94\" }",
+                          TestReflection::kNo));
+  EXPECT_TRUE(JsonAndBack("{ \"foo_string\": \"\\uD83C\\uDF32\" }",
+                          TestReflection::kNo));
+  EXPECT_FALSE(
+      JsonAndBack("{ \"foo_string\": \"\\uP890\" }", TestReflection::kNo));
+  EXPECT_FALSE(
+      JsonAndBack("{ \"foo_string\": \"\\u!FA8\" }", TestReflection::kNo));
+  EXPECT_FALSE(
+      JsonAndBack("{ \"foo_string\": \"\\uF89\" }", TestReflection::kNo));
+  EXPECT_FALSE(
+      JsonAndBack("{ \"foo_string\": \"\\uD83C\" }", TestReflection::kNo));
 }
 
 // Tests that we can handle decimal points.
@@ -246,15 +283,19 @@
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_ulong\": [ 9, 7, 1 ] }"));
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_ulong\": [  ] }"));
 
-  EXPECT_TRUE(JsonAndBack("{ \"vector_foo_float\": [ 9.0, 7.0, 1.0 ] }"));
+  EXPECT_TRUE(JsonAndBack("{ \"vector_foo_float\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_float\": [  ] }"));
-  EXPECT_TRUE(JsonAndBack("{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }"));
+  EXPECT_TRUE(JsonAndBack("{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_double\": [  ] }"));
 
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_float\": [ 9, 7, 1 ] }",
-                          "{ \"vector_foo_float\": [ 9.0, 7.0, 1.0 ] }"));
+                          "{ \"vector_foo_float\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_double\": [ 9, 7, 1 ] }",
-                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }"));
+                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
 
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_string\": [ \"bar\", \"baz\" ] }"));
   EXPECT_TRUE(JsonAndBack("{ \"vector_foo_string\": [  ] }"));
@@ -295,7 +336,8 @@
   /* foo */
   "vector_foo_double": [ 9, 7, 1 ] /* foo */
 } /* foo */)",
-                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }"));
+                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
 }
 
 // Tests that C++ style comments get stripped.
@@ -304,7 +346,38 @@
   // foo
   "vector_foo_double": [ 9, 7, 1 ] // foo
 } // foo)",
-                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }"));
+                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
+
+  // Test empty comment on its own line doesn't remove the next line.
+  EXPECT_TRUE(JsonAndBack(R"({
+  //
+  "vector_foo_double": [ 9, 7, 1 ], // foo
+  "vector_foo_float": [ 3, 1, 4 ]
+} // foo)",
+                          "{ \"vector_foo_float\": [ 3.0, 1.0, 4.0 ], "
+                          "\"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
+
+  // Test empty comment at end of line doesn't remove the next line.
+  EXPECT_TRUE(JsonAndBack(R"({
+  // foo
+  "vector_foo_double": [ 2, 7, 1 ], //
+  "vector_foo_float": [ 3, 1, 4 ]
+} // foo)",
+                          "{ \"vector_foo_float\": [ 3.0, 1.0, 4.0 ], "
+                          "\"vector_foo_double\": [ 2.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
+
+  // Test empty comment at end of document doesn't cause error.
+  EXPECT_TRUE(JsonAndBack(R"({
+  // foo
+  "vector_foo_double": [ 5, 6, 7 ], // foo
+  "vector_foo_float": [ 7, 8, 9 ]
+} //)",
+                          "{ \"vector_foo_float\": [ 7.0, 8.0, 9.0 ], "
+                          "\"vector_foo_double\": [ 5.0, 6.0, 7.0 ] }",
+                          TestReflection::kNo));
 }
 
 // Tests that mixed style comments get stripped.
@@ -316,7 +389,8 @@
 }
 // foo
 /* foo */)",
-                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }"));
+                          "{ \"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }",
+                          TestReflection::kNo));
 }
 
 // Tests that multiple arrays get properly handled.
@@ -325,7 +399,8 @@
       JsonAndBack("{ \"vector_foo_float\": [ 9, 7, 1 ], \"vector_foo_double\": "
                   "[ 9, 7, 1 ] }",
                   "{ \"vector_foo_float\": [ 9.0, 7.0, 1.0 ], "
-                  "\"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }"));
+                  "\"vector_foo_double\": [ 9.0, 7.0, 1.0 ] }",
+                  TestReflection::kNo));
 }
 
 // Tests that multiple arrays get properly handled.
diff --git a/aos/json_tokenizer.cc b/aos/json_tokenizer.cc
index d277c1e..32c9247 100644
--- a/aos/json_tokenizer.cc
+++ b/aos/json_tokenizer.cc
@@ -27,14 +27,17 @@
       // C++ style comment.  Keep consuming chars until newline, or until the
       // end of the file if this is the last line (no newline at end of file).
       while (true) {
-        ConsumeChar();
+        // First check if we are at the end of the file.
         if (AtEnd()) {
           return;
         }
+        // Then check if we are at the end of the line.
         if (Char() == '\n') {
           ++linenumber_;
           break;
         }
+        // Advance to next character and repeat.
+        ConsumeChar();
       }
     } else {
       // There is no fail.  Once we are out of whitespace (including 0 of it),
diff --git a/aos/network/BUILD b/aos/network/BUILD
index db62286..8186abb 100644
--- a/aos/network/BUILD
+++ b/aos/network/BUILD
@@ -500,6 +500,32 @@
 )
 
 aos_config(
+    name = "message_bridge_test_mismatched_configs_pi1_and_pi2_config",
+    src = "message_bridge_test_mismatched_configs_pi1_and_pi2.json",
+    flatbuffers = [
+        ":remote_message_fbs",
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:timestamp_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//aos/events:aos_config"],
+)
+
+aos_config(
+    name = "message_bridge_test_mismatched_configs_pi1_and_pi3_config",
+    src = "message_bridge_test_mismatched_configs_pi1_and_pi3.json",
+    flatbuffers = [
+        ":remote_message_fbs",
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:timestamp_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//aos/events:aos_config"],
+)
+
+aos_config(
     name = "message_bridge_auth_test_config",
     src = "message_bridge_auth_test.json",
     flatbuffers = [
@@ -594,6 +620,8 @@
     data = [
         ":message_bridge_test_combined_timestamps_common_config",
         ":message_bridge_test_common_config",
+        ":message_bridge_test_mismatched_configs_pi1_and_pi2_config",
+        ":message_bridge_test_mismatched_configs_pi1_and_pi3_config",
     ],
     flaky = True,
     shard_count = 16,
@@ -613,6 +641,23 @@
     ],
 )
 
+cc_test(
+    name = "message_bridge_server_status_test",
+    srcs = [
+        "message_bridge_server_status_test.cc",
+    ],
+    data = [
+        ":message_bridge_test_combined_timestamps_common_config",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":message_bridge_server_status",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+        "//aos/testing:path",
+    ],
+)
+
 flatbuffer_cc_library(
     name = "web_proxy_fbs",
     srcs = ["web_proxy.fbs"],
diff --git a/aos/network/message_bridge_auth_client_lib.cc b/aos/network/message_bridge_auth_client_lib.cc
index 9430489..c277b4f 100644
--- a/aos/network/message_bridge_auth_client_lib.cc
+++ b/aos/network/message_bridge_auth_client_lib.cc
@@ -42,6 +42,7 @@
       config_request_fetcher_(
           event_loop_->MakeFetcher<SctpConfigRequest>("/aos")),
       client_(SctpConfigService::NewStub(channel)) {
+  poll_timer_->set_name("poll");
   event_loop_->OnRun([this] {
     poll_timer_->Schedule(event_loop_->monotonic_now(),
                           std::chrono::milliseconds(1000));
@@ -69,7 +70,7 @@
   Status status = client_->GetActiveKey(&context, request, &response);
   if (!status.ok()) {
     LOG_EVERY_N(ERROR, 50)
-        << "Unable to retreive active SCTP authentication key from server";
+        << "Unable to retrieve active SCTP authentication key from server";
     return {};
   }
 
diff --git a/aos/network/message_bridge_client_lib.cc b/aos/network/message_bridge_client_lib.cc
index 620a069..90c0cc0 100644
--- a/aos/network/message_bridge_client_lib.cc
+++ b/aos/network/message_bridge_client_lib.cc
@@ -97,6 +97,7 @@
   message_header_builder.add_queue_index(0);
   message_header_builder.add_monotonic_remote_time(0);
   message_header_builder.add_realtime_remote_time(0);
+  message_header_builder.add_monotonic_remote_transmit_time(0);
   message_header_builder.add_remote_queue_index(0);
   fbb.Finish(message_header_builder.Finish());
 
@@ -323,12 +324,14 @@
             chrono::nanoseconds(remote_data->monotonic_sent_time())),
         realtime_clock::time_point(
             chrono::nanoseconds(remote_data->realtime_sent_time())),
+        monotonic_clock::time_point(
+            chrono::nanoseconds(remote_data->monotonic_remote_transmit_time())),
         remote_data->queue_index(), remote_boot_uuid));
 
     client_status_->SampleFilter(
         client_index_,
         monotonic_clock::time_point(
-            chrono::nanoseconds(remote_data->monotonic_sent_time())),
+            chrono::nanoseconds(remote_data->monotonic_remote_transmit_time())),
         sender->monotonic_sent_time(), remote_boot_uuid);
 
     if (stream_reply_with_timestamp_[stream]) {
@@ -339,6 +342,8 @@
           .queue_index = remote_data->queue_index(),
           .monotonic_remote_time =
               sender->monotonic_sent_time().time_since_epoch().count(),
+          .monotonic_remote_transmit_time =
+              remote_data->monotonic_remote_transmit_time(),
           .realtime_remote_time =
               sender->realtime_sent_time().time_since_epoch().count(),
           .remote_queue_index = sender->sent_queue_index(),
@@ -402,6 +407,9 @@
       timestamp.channel_index);
   message_reception_reply_.mutable_message()->mutate_monotonic_sent_time(
       timestamp.monotonic_sent_time);
+  message_reception_reply_.mutable_message()
+      ->mutate_monotonic_remote_transmit_time(
+          timestamp.monotonic_remote_transmit_time);
   message_reception_reply_.mutable_message()->mutate_realtime_sent_time(
       timestamp.realtime_sent_time);
   message_reception_reply_.mutable_message()->mutate_queue_index(
diff --git a/aos/network/message_bridge_client_lib.h b/aos/network/message_bridge_client_lib.h
index 56d2563..97d5946 100644
--- a/aos/network/message_bridge_client_lib.h
+++ b/aos/network/message_bridge_client_lib.h
@@ -57,6 +57,7 @@
     int64_t realtime_sent_time;
     uint32_t queue_index;
     int64_t monotonic_remote_time;
+    int64_t monotonic_remote_transmit_time;
     int64_t realtime_remote_time;
     uint32_t remote_queue_index;
   };
diff --git a/aos/network/message_bridge_retry_test.cc b/aos/network/message_bridge_retry_test.cc
index 8229c99..e90bcb0 100644
--- a/aos/network/message_bridge_retry_test.cc
+++ b/aos/network/message_bridge_retry_test.cc
@@ -38,25 +38,25 @@
 TEST_P(MessageBridgeParameterizedTest, ReliableRetries) {
   // Set an absurdly small wmem max. This will help to trigger retries.
   FLAGS_force_wmem_max = 1024;
-  OnPi1();
+  pi1_.OnPi();
 
   FLAGS_application_name = "sender";
-  aos::ShmEventLoop send_event_loop(&config.message());
+  aos::ShmEventLoop send_event_loop(&config_.message());
   aos::Sender<examples::Ping> ping_sender =
       send_event_loop.MakeSender<examples::Ping>("/test");
   SendPing(&ping_sender, 1);
   aos::Fetcher<ServerStatistics> pi1_server_statistics_fetcher =
       send_event_loop.MakeFetcher<ServerStatistics>("/aos");
 
-  MakePi1Server();
-  MakePi1Client();
+  pi1_.MakeServer();
+  pi1_.MakeClient();
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
+  pi2_.OnPi();
 
-  MakePi2Server();
+  pi2_.MakeServer();
 
-  aos::ShmEventLoop receive_event_loop(&config.message());
+  aos::ShmEventLoop receive_event_loop(&config_.message());
   aos::Fetcher<examples::Ping> ping_fetcher =
       receive_event_loop.MakeFetcher<examples::Ping>("/test");
   aos::Fetcher<ClientStatistics> pi2_client_statistics_fetcher =
@@ -66,15 +66,15 @@
   EXPECT_FALSE(ping_fetcher.Fetch());
 
   // Spin up the persistent pieces.
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Server();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartServer();
 
   {
     constexpr size_t kNumPingMessages = 25;
     // Now, spin up a client for 2 seconds.
-    MakePi2Client();
-    StartPi2Client();
+    pi2_.MakeClient();
+    pi2_.StartClient();
 
     std::this_thread::sleep_for(std::chrono::seconds(2));
 
@@ -85,7 +85,7 @@
     // Give plenty of time for retries to succeed.
     std::this_thread::sleep_for(std::chrono::seconds(5));
 
-    StopPi2Client();
+    pi2_.StopClient();
 
     // Confirm there is no detected duplicate packet.
     EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
@@ -124,9 +124,9 @@
   }
 
   // Shut everyone else down.
-  StopPi1Client();
-  StopPi2Server();
-  StopPi1Server();
+  pi1_.StopClient();
+  pi2_.StopServer();
+  pi1_.StopServer();
 }
 
 INSTANTIATE_TEST_SUITE_P(MessageBridgeTests, MessageBridgeParameterizedTest,
diff --git a/aos/network/message_bridge_server_lib.cc b/aos/network/message_bridge_server_lib.cc
index a2df830..3c7a8d2 100644
--- a/aos/network/message_bridge_server_lib.cc
+++ b/aos/network/message_bridge_server_lib.cc
@@ -95,6 +95,8 @@
       context.realtime_event_time.time_since_epoch().count());
   remote_data_builder.add_data(data_offset);
   remote_data_builder.add_boot_uuid(boot_uuid_offset);
+  remote_data_builder.add_monotonic_remote_transmit_time(
+      monotonic_clock::now().time_since_epoch().count());
 
   // TODO(austin): Use an iovec to build it up in 3 parts to avoid the copy?
   // Only useful when not logging.
@@ -304,6 +306,8 @@
         remote_message_builder.add_remote_queue_index(
             message_header->queue_index());
         remote_message_builder.add_boot_uuid(boot_uuid_offset);
+        remote_message_builder.add_monotonic_remote_transmit_time(
+            message_header->monotonic_remote_transmit_time());
 
         server_status->AddPartialDeliveries(peer.node_index,
                                             partial_deliveries);
@@ -418,7 +422,10 @@
       timestamp_loggers_(event_loop_),
       server_(max_channels() + kControlStreams(), "",
               event_loop->node()->port(), requested_authentication),
-      server_status_(event_loop, [this]() { timestamp_state_->SendData(); }),
+      server_status_(event_loop,
+                     [this](uint32_t, monotonic_clock::time_point) {
+                       timestamp_state_->SendData();
+                     }),
       config_sha256_(std::move(config_sha256)),
       allocator_(0),
       refresh_key_timer_(event_loop->AddTimer([this]() { RequestAuthKey(); })),
@@ -672,44 +679,13 @@
       HandleData(message.get());
       break;
     case Message::kOverflow:
-      MaybeIncrementInvalidConnectionCount(nullptr);
+      server_status_.MaybeIncrementInvalidConnectionCount(nullptr);
       NodeDisconnected(message->header.rcvinfo.rcv_assoc_id);
       break;
   }
   server_.FreeMessage(std::move(message));
 }
 
-void MessageBridgeServer::MaybeIncrementInvalidConnectionCount(
-    const Node *node) {
-  server_status_.increment_invalid_connection_count();
-
-  if (node == nullptr) {
-    return;
-  }
-
-  if (!node->has_name()) {
-    return;
-  }
-
-  const aos::Node *client_node = configuration::GetNode(
-      event_loop_->configuration(), node->name()->string_view());
-
-  if (client_node == nullptr) {
-    return;
-  }
-
-  const int node_index =
-      configuration::GetNodeIndex(event_loop_->configuration(), client_node);
-
-  ServerConnection *connection =
-      server_status_.nodes()[node_index].value().server_connection;
-
-  if (connection != nullptr) {
-    connection->mutate_invalid_connection_count(
-        connection->invalid_connection_count() + 1);
-  }
-}
-
 void MessageBridgeServer::HandleData(const Message *message) {
   VLOG(2) << "Received data of length " << message->size;
 
@@ -725,7 +701,7 @@
         }
         server_.Abort(message->header.rcvinfo.rcv_assoc_id);
 
-        MaybeIncrementInvalidConnectionCount(nullptr);
+        server_status_.MaybeIncrementInvalidConnectionCount(nullptr);
         return;
       }
     }
@@ -737,7 +713,7 @@
       }
       server_.Abort(message->header.rcvinfo.rcv_assoc_id);
 
-      MaybeIncrementInvalidConnectionCount(connect->node());
+      server_status_.MaybeIncrementInvalidConnectionCount(connect->node());
       return;
     }
 
@@ -750,7 +726,7 @@
       }
       server_.Abort(message->header.rcvinfo.rcv_assoc_id);
 
-      MaybeIncrementInvalidConnectionCount(connect->node());
+      server_status_.MaybeIncrementInvalidConnectionCount(connect->node());
       return;
     }
 
@@ -762,7 +738,7 @@
       }
       server_.Abort(message->header.rcvinfo.rcv_assoc_id);
 
-      MaybeIncrementInvalidConnectionCount(connect->node());
+      server_status_.MaybeIncrementInvalidConnectionCount(connect->node());
       return;
     }
 
@@ -803,7 +779,7 @@
         }
         server_.Abort(message->header.rcvinfo.rcv_assoc_id);
 
-        MaybeIncrementInvalidConnectionCount(connect->node());
+        server_status_.MaybeIncrementInvalidConnectionCount(connect->node());
         return;
       }
       ++channel_index;
diff --git a/aos/network/message_bridge_server_lib.h b/aos/network/message_bridge_server_lib.h
index b47a4e6..1c3903f 100644
--- a/aos/network/message_bridge_server_lib.h
+++ b/aos/network/message_bridge_server_lib.h
@@ -192,10 +192,6 @@
   // received.
   void HandleData(const Message *message);
 
-  // Increments the invalid connection count overall, and per node if we know
-  // which node (ie, node is not nullptr).
-  void MaybeIncrementInvalidConnectionCount(const Node *node);
-
   // The maximum number of channels we support on a single connection. We need
   // to configure the SCTP socket with this before any clients connect, so we
   // need an upper bound on the number of channels any of them will use.
diff --git a/aos/network/message_bridge_server_status.cc b/aos/network/message_bridge_server_status.cc
index 0e8c6b0..e893b40 100644
--- a/aos/network/message_bridge_server_status.cc
+++ b/aos/network/message_bridge_server_status.cc
@@ -90,7 +90,8 @@
 }  // namespace
 
 MessageBridgeServerStatus::MessageBridgeServerStatus(
-    aos::EventLoop *event_loop, std::function<void()> send_data)
+    aos::EventLoop *event_loop,
+    std::function<void(uint32_t, monotonic_clock::time_point)> send_data)
     : event_loop_(event_loop),
       sender_(event_loop_->MakeSender<ServerStatistics>("/aos")),
       statistics_(MakeServerStatistics(
@@ -100,7 +101,7 @@
       client_statistics_fetcher_(
           event_loop_->MakeFetcher<ClientStatistics>("/aos")),
       timestamp_sender_(event_loop_->MakeSender<Timestamp>("/aos")),
-      send_data_(send_data) {
+      send_data_(std::move(send_data)) {
   server_connection_offsets_.reserve(
       statistics_.message().connections()->size());
   client_offsets_.reserve(statistics_.message().connections()->size());
@@ -484,7 +485,8 @@
     // Since we are building up the timestamp to send here, we need to trigger
     // the SendData call ourselves.
     if (send_data_) {
-      send_data_();
+      send_data_(timestamp_sender_.sent_queue_index(),
+                 timestamp_sender_.monotonic_sent_time());
     }
   }
 }
@@ -506,4 +508,48 @@
                               kPingPeriod);
 }
 
+void MessageBridgeServerStatus::MaybeIncrementInvalidConnectionCount(
+    const Node *node) {
+  increment_invalid_connection_count();
+
+  if (node == nullptr) {
+    return;
+  }
+
+  if (!node->has_name()) {
+    return;
+  }
+
+  const aos::Node *client_node = configuration::GetNode(
+      event_loop_->configuration(), node->name()->string_view());
+
+  if (client_node == nullptr) {
+    return;
+  }
+
+  const int node_index =
+      configuration::GetNodeIndex(event_loop_->configuration(), client_node);
+
+  const std::vector<std::optional<MessageBridgeServerStatus::NodeState>>
+      &server_nodes = nodes();
+  // There is a chance that there is no server node for the given client
+  // `node_index`. This can happen if the other node has a different
+  // configuration such that it starts forwarding messages to the current node,
+  // but the current node's configuration does not expect messages from the
+  // other node. This is likely to happen during a multi-node software update
+  // where the other node has been updated with a different config, while the
+  // current node's update hasn't yet completed. In such cases, we want to
+  // ensure that a server node exists before attempting to access it.
+  if (!server_nodes[node_index]) {
+    return;
+  }
+  ServerConnection *connection =
+      server_nodes[node_index].value().server_connection;
+
+  if (connection != nullptr) {
+    connection->mutate_invalid_connection_count(
+        connection->invalid_connection_count() + 1);
+  }
+}
+
 }  // namespace aos::message_bridge
diff --git a/aos/network/message_bridge_server_status.h b/aos/network/message_bridge_server_status.h
index 8bdf4a8..11b74a0 100644
--- a/aos/network/message_bridge_server_status.h
+++ b/aos/network/message_bridge_server_status.h
@@ -52,7 +52,8 @@
 
   MessageBridgeServerStatus(
       aos::EventLoop *event_loop,
-      std::function<void()> send_data = std::function<void()>());
+      std::function<void(uint32_t, monotonic_clock::time_point)> send_data =
+          std::function<void(uint32_t, monotonic_clock::time_point)>());
 
   MessageBridgeServerStatus(const MessageBridgeServerStatus &) = delete;
   MessageBridgeServerStatus(MessageBridgeServerStatus &&) = delete;
@@ -60,7 +61,8 @@
       delete;
   MessageBridgeServerStatus &operator=(MessageBridgeServerStatus &&) = delete;
 
-  void set_send_data(std::function<void()> send_data) {
+  void set_send_data(
+      std::function<void(uint32_t, monotonic_clock::time_point)> send_data) {
     send_data_ = send_data;
   }
 
@@ -113,6 +115,10 @@
   // connection that got rejected.
   void increment_invalid_connection_count() { ++invalid_connection_count_; }
 
+  // Increments the invalid connection count overall, and per node if we know
+  // which node (ie, node is not nullptr).
+  void MaybeIncrementInvalidConnectionCount(const Node *node);
+
  private:
   static constexpr std::chrono::nanoseconds kStatisticsPeriod =
       std::chrono::seconds(1);
@@ -148,7 +154,7 @@
   aos::monotonic_clock::time_point last_statistics_send_time_ =
       aos::monotonic_clock::min_time;
 
-  std::function<void()> send_data_;
+  std::function<void(uint32_t, monotonic_clock::time_point)> send_data_;
 
   bool send_ = true;
 
diff --git a/aos/network/message_bridge_server_status_test.cc b/aos/network/message_bridge_server_status_test.cc
new file mode 100644
index 0000000..8b9aefa
--- /dev/null
+++ b/aos/network/message_bridge_server_status_test.cc
@@ -0,0 +1,44 @@
+#include "aos/network/message_bridge_server_status.h"
+
+#include "gtest/gtest.h"
+
+#include "aos/events/simulated_event_loop.h"
+
+namespace aos::message_bridge::testing {
+
+TEST(MessageBridgeServerStatus, NoThrowOnInvalidServerNode) {
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config(
+      aos::configuration::ReadConfig(
+          "message_bridge_test_combined_timestamps_common_config.json"));
+  aos::SimulatedEventLoopFactory factory(&config.message());
+  // Configure the server node to be `pi1` - for details
+  // on the configuration, refer to
+  // `message_bridge_test_combined_timestamps_common.json`.
+  std::unique_ptr<EventLoop> event_loop =
+      factory.GetNodeEventLoopFactory("pi1")->MakeEventLoop("test");
+  MessageBridgeServerStatus server_status(event_loop.get());
+  // We want to choose a client node such that there is no server for that
+  // client on this node. A simple way to do this is to choose the client node
+  // to be the same as the server node. There will never be a valid `NodeState`
+  // object assigned in `MessageBridgeServerStatus::nodes_`, which is an
+  // `std::vector` of `std::optional<NodeState> elements`. This is because a
+  // node will not be allowed to forward messages to itself since that would
+  // cause a loop of the same message being forwarded over-and-over again. We're
+  // making use of this property to simulate a multi-node software update
+  // scenario in which one node was upgraded to a config that had a valid
+  // connection to another node and started forwarding messages to the other
+  // node. Since the other node was in the process of being updated to the new
+  // software, it did not have the updated config yet, and couldn't find a
+  // server node corresponding to the client node. In this situation,
+  // `MaybeIncrementInvalidConnectionCount()` ended-up accessing an
+  // `std::optional` that was unset. As a regression test, we want to ensure
+  // that no exceptions are raised in this scenario, now that the proper checks
+  // have been added.
+  const aos::Node *client_node =
+      aos::configuration::GetNode(&config.message(), "pi1");
+  EXPECT_NE(client_node, nullptr);
+  EXPECT_NO_THROW(
+      server_status.MaybeIncrementInvalidConnectionCount(client_node));
+}
+
+}  // namespace aos::message_bridge::testing
\ No newline at end of file
diff --git a/aos/network/message_bridge_test.cc b/aos/network/message_bridge_test.cc
index c211c89..2dac8ed 100644
--- a/aos/network/message_bridge_test.cc
+++ b/aos/network/message_bridge_test.cc
@@ -45,23 +45,23 @@
   // hope for the best.  We can be more generous in the future if we need to.
   //
   // We are faking the application names by passing in --application_name=foo
-  OnPi1();
+  pi1_.OnPi();
   // Force ourselves to be "raspberrypi" and allocate everything.
 
-  MakePi1Server();
-  MakePi1Client();
+  pi1_.MakeServer();
+  pi1_.MakeClient();
 
   const std::string long_data = std::string(10000, 'a');
 
   // And build the app which sends the pings.
   FLAGS_application_name = "ping";
-  aos::ShmEventLoop ping_event_loop(&config.message());
+  aos::ShmEventLoop ping_event_loop(&config_.message());
   aos::Sender<examples::Ping> ping_sender =
       ping_event_loop.MakeSender<examples::Ping>("/test");
 
-  aos::ShmEventLoop pi1_test_event_loop(&config.message());
+  aos::ShmEventLoop pi1_test_event_loop_(&pi1_.config_.message());
   aos::Fetcher<RemoteMessage> message_header_fetcher1 =
-      pi1_test_event_loop.MakeFetcher<RemoteMessage>(
+      pi1_test_event_loop_.MakeFetcher<RemoteMessage>(
           shared() ? "/pi1/aos/remote_timestamps/pi2"
                    : "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping");
 
@@ -72,18 +72,18 @@
       ping_event_loop.MakeFetcher<Timestamp>("/aos");
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
+  pi2_.OnPi();
 
-  MakePi2Client();
-  MakePi2Server();
+  pi2_.MakeClient();
+  pi2_.MakeServer();
 
   // And build the app which sends the pongs.
   FLAGS_application_name = "pong";
-  aos::ShmEventLoop pong_event_loop(&config.message());
+  aos::ShmEventLoop pong_event_loop(&config_.message());
 
   // And build the app for testing.
   FLAGS_application_name = "test";
-  aos::ShmEventLoop test_event_loop(&config.message());
+  aos::ShmEventLoop test_event_loop(&config_.message());
 
   aos::Fetcher<ClientStatistics> client_statistics_fetcher =
       test_event_loop.MakeFetcher<ClientStatistics>("/aos");
@@ -95,7 +95,7 @@
 
   // Event loop for fetching data delivered to pi2 from pi1 to match up
   // messages.
-  aos::ShmEventLoop delivered_messages_event_loop(&config.message());
+  aos::ShmEventLoop delivered_messages_event_loop(&config_.message());
   aos::Fetcher<Timestamp> pi1_on_pi2_timestamp_fetcher =
       delivered_messages_event_loop.MakeFetcher<Timestamp>("/pi1/aos");
   aos::Fetcher<examples::Ping> ping_on_pi2_fetcher =
@@ -107,7 +107,7 @@
   int pong_count = 0;
   pong_event_loop.MakeWatcher("/test", [&pong_count, &pong_event_loop,
                                         this](const examples::Ping &ping) {
-    EXPECT_EQ(pong_event_loop.context().source_boot_uuid, pi1_boot_uuid_);
+    EXPECT_EQ(pong_event_loop.context().source_boot_uuid, pi1_.boot_uuid_);
     ++pong_count;
     VLOG(1) << "Got ping back " << FlatbufferToJson(&ping);
   });
@@ -139,7 +139,7 @@
           }
 
           if (connection->node()->name()->string_view() ==
-              pi2_client_event_loop->node()->name()->string_view()) {
+              pi2_.client_event_loop_->node()->name()->string_view()) {
             if (connection->state() == State::CONNECTED) {
               EXPECT_TRUE(connection->has_boot_uuid());
               EXPECT_EQ(connection->connection_count(), 1u);
@@ -344,6 +344,9 @@
               chrono::nanoseconds(header.realtime_sent_time()));
           const aos::monotonic_clock::time_point header_monotonic_remote_time(
               chrono::nanoseconds(header.monotonic_remote_time()));
+          const aos::monotonic_clock::time_point
+              header_monotonic_remote_transmit_time(
+                  chrono::nanoseconds(header.monotonic_remote_transmit_time()));
           const aos::realtime_clock::time_point header_realtime_remote_time(
               chrono::nanoseconds(header.realtime_remote_time()));
 
@@ -396,12 +399,19 @@
           EXPECT_EQ(pi2_context->monotonic_remote_time,
                     header_monotonic_remote_time);
 
+          EXPECT_LT(header_monotonic_remote_transmit_time,
+                    pi2_context->monotonic_event_time);
+          EXPECT_GT(header_monotonic_remote_transmit_time,
+                    pi2_context->monotonic_remote_time);
+
           // Confirm the forwarded message also matches the source message.
           EXPECT_EQ(pi1_context->queue_index, header.queue_index());
           EXPECT_EQ(pi1_context->monotonic_event_time,
                     header_monotonic_remote_time);
           EXPECT_EQ(pi1_context->realtime_event_time,
                     header_realtime_remote_time);
+          EXPECT_EQ(header_monotonic_remote_transmit_time,
+                    pi2_context->monotonic_remote_transmit_time);
         });
   }
 
@@ -410,10 +420,10 @@
   ThreadedEventLoopRunner pong_thread(&pong_event_loop);
   ThreadedEventLoopRunner ping_thread(&ping_event_loop);
 
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Client();
-  StartPi2Server();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartClient();
+  pi2_.StartServer();
 
   // And go!
   // Run for 5 seconds to make sure we have time to estimate the offset.
@@ -442,10 +452,10 @@
   // Shut everyone else down before confirming everything actually ran.
   ping_thread.Exit();
   pong_thread.Exit();
-  StopPi1Server();
-  StopPi1Client();
-  StopPi2Client();
-  StopPi2Server();
+  pi1_.StopServer();
+  pi1_.StopClient();
+  pi2_.StopClient();
+  pi2_.StopServer();
 
   // Make sure we sent something.
   EXPECT_GE(ping_count, 1);
@@ -479,37 +489,37 @@
   // hope for the best.  We can be more generous in the future if we need to.
   //
   // We are faking the application names by passing in --application_name=foo
-  OnPi1();
+  pi1_.OnPi();
 
-  MakePi1Server();
-  MakePi1Client();
+  pi1_.MakeServer();
+  pi1_.MakeClient();
 
   // And build the app for testing.
-  MakePi1Test();
+  pi1_.MakeTest("test1", &pi2_);
   aos::Fetcher<ServerStatistics> pi1_server_statistics_fetcher =
-      pi1_test_event_loop->MakeFetcher<ServerStatistics>("/pi1/aos");
+      pi1_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi1/aos");
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
-  MakePi2Server();
+  pi2_.OnPi();
+  pi2_.MakeServer();
 
   // And build the app for testing.
-  MakePi2Test();
+  pi2_.MakeTest("test2", &pi1_);
   aos::Fetcher<ServerStatistics> pi2_server_statistics_fetcher =
-      pi2_test_event_loop->MakeFetcher<ServerStatistics>("/pi2/aos");
+      pi2_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi2/aos");
 
   // Wait until we are connected, then send.
 
-  StartPi1Test();
-  StartPi2Test();
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Server();
+  pi1_.StartTest();
+  pi2_.StartTest();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartServer();
 
   {
-    MakePi2Client();
+    pi2_.MakeClient();
 
-    RunPi2Client(chrono::milliseconds(3050));
+    pi2_.RunClient(chrono::milliseconds(3050));
 
     // Now confirm we are synchronized.
     EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
@@ -540,7 +550,7 @@
               chrono::milliseconds(-1));
     EXPECT_TRUE(pi2_connection->has_boot_uuid());
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   std::this_thread::sleep_for(SctpClientConnection::kReconnectTimeout +
@@ -568,9 +578,9 @@
   }
 
   {
-    MakePi2Client();
+    pi2_.MakeClient();
     // And go!
-    RunPi2Client(chrono::milliseconds(3050));
+    pi2_.RunClient(chrono::milliseconds(3050));
 
     EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
     EXPECT_TRUE(pi2_server_statistics_fetcher.Fetch());
@@ -605,15 +615,15 @@
         << ": " << FlatbufferToJson(pi2_connection);
     EXPECT_TRUE(pi2_connection->has_boot_uuid());
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   // Shut everyone else down.
-  StopPi1Server();
-  StopPi1Client();
-  StopPi2Server();
-  StopPi1Test();
-  StopPi2Test();
+  pi1_.StopServer();
+  pi1_.StopClient();
+  pi2_.StopServer();
+  pi1_.StopTest();
+  pi2_.StopTest();
 }
 
 // Test that the server disconnecting triggers the server offsets on the other
@@ -634,41 +644,41 @@
   //
   // We are faking the application names by passing in --application_name=foo
   // Force ourselves to be "raspberrypi" and allocate everything.
-  OnPi1();
-  MakePi1Server();
-  MakePi1Client();
+  pi1_.OnPi();
+  pi1_.MakeServer();
+  pi1_.MakeClient();
 
   // And build the app for testing.
-  MakePi1Test();
+  pi1_.MakeTest("test1", &pi2_);
   aos::Fetcher<ServerStatistics> pi1_server_statistics_fetcher =
-      pi1_test_event_loop->MakeFetcher<ServerStatistics>("/pi1/aos");
+      pi1_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi1/aos");
   aos::Fetcher<ClientStatistics> pi1_client_statistics_fetcher =
-      pi1_test_event_loop->MakeFetcher<ClientStatistics>("/pi1/aos");
+      pi1_.test_event_loop_->MakeFetcher<ClientStatistics>("/pi1/aos");
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
-  MakePi2Client();
+  pi2_.OnPi();
+  pi2_.MakeClient();
 
   // And build the app for testing.
-  MakePi2Test();
+  pi2_.MakeTest("test1", &pi1_);
   aos::Fetcher<ServerStatistics> pi2_server_statistics_fetcher =
-      pi2_test_event_loop->MakeFetcher<ServerStatistics>("/pi2/aos");
+      pi2_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi2/aos");
 
   // Start everything up.  Pong is the only thing we don't know how to wait on,
   // so start it first.
-  StartPi1Test();
-  StartPi2Test();
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Client();
+  pi1_.StartTest();
+  pi2_.StartTest();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartClient();
 
   // Confirm both client and server statistics messages have decent offsets in
   // them.
 
   {
-    MakePi2Server();
+    pi2_.MakeServer();
 
-    RunPi2Server(chrono::milliseconds(3050));
+    pi2_.RunServer(chrono::milliseconds(3050));
 
     // Now confirm we are synchronized.
     EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
@@ -699,7 +709,7 @@
     EXPECT_TRUE(pi2_connection->has_connected_since_time());
     EXPECT_EQ(pi2_connection->connection_count(), 1u);
 
-    StopPi2Server();
+    pi2_.StopServer();
   }
 
   std::this_thread::sleep_for(std::chrono::seconds(2));
@@ -728,11 +738,11 @@
   }
 
   {
-    MakePi2Server();
+    pi2_.MakeServer();
 
     // Wait long enough for the client to connect again.  It currently takes 3
     // seconds of connection to estimate the time offset.
-    RunPi2Server(chrono::milliseconds(4050));
+    pi2_.RunServer(chrono::milliseconds(4050));
 
     // And confirm we are synchronized again.
     EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
@@ -767,15 +777,15 @@
               chrono::milliseconds(-1));
     EXPECT_TRUE(pi2_connection->has_boot_uuid());
 
-    StopPi2Server();
+    pi2_.StopServer();
   }
 
   // Shut everyone else down.
-  StopPi1Server();
-  StopPi1Client();
-  StopPi2Client();
-  StopPi1Test();
-  StopPi2Test();
+  pi1_.StopServer();
+  pi1_.StopClient();
+  pi2_.StopClient();
+  pi1_.StopTest();
+  pi2_.StopTest();
 }
 
 // TODO(austin): The above test confirms that the external state does the right
@@ -793,10 +803,10 @@
 // Tests that when a message is sent before the bridge starts up, but is
 // configured as reliable, we forward it.  Confirm this survives a client reset.
 TEST_P(MessageBridgeParameterizedTest, ReliableSentBeforeClientStartup) {
-  OnPi1();
+  pi1_.OnPi();
 
   FLAGS_application_name = "sender";
-  aos::ShmEventLoop send_event_loop(&config.message());
+  aos::ShmEventLoop send_event_loop(&config_.message());
   aos::Sender<examples::Ping> ping_sender =
       send_event_loop.MakeSender<examples::Ping>("/test");
   SendPing(&ping_sender, 1);
@@ -804,18 +814,18 @@
       send_event_loop.MakeSender<examples::Ping>("/unreliable");
   SendPing(&unreliable_ping_sender, 1);
 
-  MakePi1Server();
-  MakePi1Client();
+  pi1_.MakeServer();
+  pi1_.MakeClient();
 
   FLAGS_application_name = "pi1_timestamp";
-  aos::ShmEventLoop pi1_remote_timestamp_event_loop(&config.message());
+  aos::ShmEventLoop pi1_remote_timestamp_event_loop(&config_.message());
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
+  pi2_.OnPi();
 
-  MakePi2Server();
+  pi2_.MakeServer();
 
-  aos::ShmEventLoop receive_event_loop(&config.message());
+  aos::ShmEventLoop receive_event_loop(&config_.message());
   aos::Fetcher<examples::Ping> ping_fetcher =
       receive_event_loop.MakeFetcher<examples::Ping>("/test");
   aos::Fetcher<examples::Ping> unreliable_ping_fetcher =
@@ -850,9 +860,9 @@
   EXPECT_FALSE(unreliable_ping_fetcher.Fetch());
 
   // Spin up the persistent pieces.
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Server();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartServer();
 
   // Event used to wait for the timestamp counting thread to start.
   std::unique_ptr<ThreadedEventLoopRunner> pi1_remote_timestamp_thread =
@@ -860,10 +870,12 @@
           &pi1_remote_timestamp_event_loop);
 
   {
+    const aos::monotonic_clock::time_point startup_time =
+        aos::monotonic_clock::now();
     // Now spin up a client for 2 seconds.
-    MakePi2Client();
+    pi2_.MakeClient();
 
-    RunPi2Client(chrono::milliseconds(2050));
+    pi2_.RunClient(chrono::milliseconds(2050));
 
     // Confirm there is no detected duplicate packet.
     EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
@@ -878,17 +890,21 @@
               0u);
 
     EXPECT_TRUE(ping_fetcher.Fetch());
+    EXPECT_GT(ping_fetcher.context().monotonic_remote_transmit_time,
+              startup_time);
+    EXPECT_LT(ping_fetcher.context().monotonic_remote_transmit_time,
+              aos::monotonic_clock::now());
     EXPECT_FALSE(unreliable_ping_fetcher.Fetch());
     EXPECT_EQ(ping_timestamp_count, 1);
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   {
     // Now, spin up a client for 2 seconds.
-    MakePi2Client();
+    pi2_.MakeClient();
 
-    RunPi2Client(chrono::milliseconds(5050));
+    pi2_.RunClient(chrono::milliseconds(5050));
 
     // Confirm we detect the duplicate packet correctly.
     EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
@@ -906,14 +922,14 @@
     EXPECT_FALSE(ping_fetcher.Fetch());
     EXPECT_FALSE(unreliable_ping_fetcher.Fetch());
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   // Shut everyone else down.
-  StopPi1Client();
-  StopPi2Server();
+  pi1_.StopClient();
+  pi2_.StopServer();
   pi1_remote_timestamp_thread.reset();
-  StopPi1Server();
+  pi1_.StopServer();
 }
 
 // Tests that when a message is sent before the bridge starts up, but is
@@ -921,12 +937,12 @@
 // resets.
 TEST_P(MessageBridgeParameterizedTest, ReliableSentBeforeServerStartup) {
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
+  pi2_.OnPi();
 
-  MakePi2Server();
-  MakePi2Client();
+  pi2_.MakeServer();
+  pi2_.MakeClient();
 
-  aos::ShmEventLoop receive_event_loop(&config.message());
+  aos::ShmEventLoop receive_event_loop(&config_.message());
   aos::Fetcher<examples::Ping> ping_fetcher =
       receive_event_loop.MakeFetcher<examples::Ping>("/test");
   aos::Fetcher<examples::Ping> unreliable_ping_fetcher =
@@ -935,10 +951,10 @@
       receive_event_loop.MakeFetcher<ClientStatistics>("/pi2/aos");
 
   // Force ourselves to be "raspberrypi" and allocate everything.
-  OnPi1();
+  pi1_.OnPi();
 
   FLAGS_application_name = "sender";
-  aos::ShmEventLoop send_event_loop(&config.message());
+  aos::ShmEventLoop send_event_loop(&config_.message());
   aos::Sender<examples::Ping> ping_sender =
       send_event_loop.MakeSender<examples::Ping>("/test");
   {
@@ -949,10 +965,10 @@
     builder.CheckOk(builder.Send(ping_builder.Finish()));
   }
 
-  MakePi1Client();
+  pi1_.MakeClient();
 
   FLAGS_application_name = "pi1_timestamp";
-  aos::ShmEventLoop pi1_remote_timestamp_event_loop(&config.message());
+  aos::ShmEventLoop pi1_remote_timestamp_event_loop(&config_.message());
 
   const size_t ping_channel_index = configuration::ChannelIndex(
       receive_event_loop.configuration(), ping_fetcher.channel());
@@ -981,19 +997,21 @@
   EXPECT_FALSE(unreliable_ping_fetcher.Fetch());
 
   // Spin up the persistent pieces.
-  StartPi1Client();
-  StartPi2Server();
-  StartPi2Client();
+  pi1_.StartClient();
+  pi2_.StartServer();
+  pi2_.StartClient();
 
   std::unique_ptr<ThreadedEventLoopRunner> pi1_remote_timestamp_thread =
       std::make_unique<ThreadedEventLoopRunner>(
           &pi1_remote_timestamp_event_loop);
 
   {
+    const aos::monotonic_clock::time_point startup_time =
+        aos::monotonic_clock::now();
     // Now, spin up a server for 2 seconds.
-    MakePi1Server();
+    pi1_.MakeServer();
 
-    RunPi1Server(chrono::milliseconds(2050));
+    pi1_.RunServer(chrono::milliseconds(2050));
 
     // Confirm there is no detected duplicate packet.
     EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
@@ -1008,18 +1026,23 @@
               0u);
 
     EXPECT_TRUE(ping_fetcher.Fetch());
+    EXPECT_GT(ping_fetcher.context().monotonic_remote_transmit_time,
+              startup_time);
+    EXPECT_LT(ping_fetcher.context().monotonic_remote_transmit_time,
+              aos::monotonic_clock::now());
+
     EXPECT_FALSE(unreliable_ping_fetcher.Fetch());
     EXPECT_EQ(ping_timestamp_count, 1);
     LOG(INFO) << "Shutting down first pi1 MessageBridgeServer";
 
-    StopPi1Server();
+    pi1_.StopServer();
   }
 
   {
     // Now, spin up a second server for 2 seconds.
-    MakePi1Server();
+    pi1_.MakeServer();
 
-    RunPi1Server(chrono::milliseconds(2050));
+    pi1_.RunServer(chrono::milliseconds(2050));
 
     // Confirm we detect the duplicate packet correctly.
     EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
@@ -1037,13 +1060,13 @@
     EXPECT_FALSE(ping_fetcher.Fetch());
     EXPECT_FALSE(unreliable_ping_fetcher.Fetch());
 
-    StopPi1Server();
+    pi1_.StopServer();
   }
 
   // Shut everyone else down.
-  StopPi1Client();
-  StopPi2Server();
-  StopPi2Client();
+  pi1_.StopClient();
+  pi2_.StopServer();
+  pi2_.StopClient();
   pi1_remote_timestamp_thread.reset();
 }
 
@@ -1052,27 +1075,27 @@
 // client. This ensures that we handle a disconnecting & reconnecting client
 // correctly in the server reliable connection retry logic.
 TEST_P(MessageBridgeParameterizedTest, ReliableSentDuringClientReboot) {
-  OnPi1();
+  pi1_.OnPi();
 
   FLAGS_application_name = "sender";
-  aos::ShmEventLoop send_event_loop(&config.message());
+  aos::ShmEventLoop send_event_loop(&config_.message());
   aos::Sender<examples::Ping> ping_sender =
       send_event_loop.MakeSender<examples::Ping>("/test");
   size_t ping_index = 0;
   SendPing(&ping_sender, ++ping_index);
 
-  MakePi1Server();
-  MakePi1Client();
+  pi1_.MakeServer();
+  pi1_.MakeClient();
 
   FLAGS_application_name = "pi1_timestamp";
-  aos::ShmEventLoop pi1_remote_timestamp_event_loop(&config.message());
+  aos::ShmEventLoop pi1_remote_timestamp_event_loop(&config_.message());
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
+  pi2_.OnPi();
 
-  MakePi2Server();
+  pi2_.MakeServer();
 
-  aos::ShmEventLoop receive_event_loop(&config.message());
+  aos::ShmEventLoop receive_event_loop(&config_.message());
   aos::Fetcher<examples::Ping> ping_fetcher =
       receive_event_loop.MakeFetcher<examples::Ping>("/test");
   aos::Fetcher<ClientStatistics> pi2_client_statistics_fetcher =
@@ -1104,9 +1127,9 @@
   EXPECT_FALSE(ping_fetcher.Fetch());
 
   // Spin up the persistent pieces.
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Server();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartServer();
 
   // Event used to wait for the timestamp counting thread to start.
   std::unique_ptr<ThreadedEventLoopRunner> pi1_remote_timestamp_thread =
@@ -1115,9 +1138,9 @@
 
   {
     // Now, spin up a client for 2 seconds.
-    MakePi2Client();
+    pi2_.MakeClient();
 
-    RunPi2Client(chrono::milliseconds(2050));
+    pi2_.RunClient(chrono::milliseconds(2050));
 
     // Confirm there is no detected duplicate packet.
     EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
@@ -1134,7 +1157,7 @@
     EXPECT_TRUE(ping_fetcher.Fetch());
     EXPECT_EQ(ping_timestamp_count, 1);
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   // Send some reliable messages while the client is dead. Only the final one
@@ -1144,10 +1167,12 @@
   }
 
   {
+    const aos::monotonic_clock::time_point startup_time =
+        aos::monotonic_clock::now();
     // Now, spin up a client for 2 seconds.
-    MakePi2Client();
+    pi2_.MakeClient();
 
-    RunPi2Client(chrono::milliseconds(5050));
+    pi2_.RunClient(chrono::milliseconds(5050));
 
     // No duplicate packets should have appeared.
     EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
@@ -1165,17 +1190,22 @@
     // We should have gotten precisely one more ping message--the latest one
     // sent should've made it, but no previous ones.
     EXPECT_TRUE(ping_fetcher.FetchNext());
+    EXPECT_GT(ping_fetcher.context().monotonic_remote_transmit_time,
+              startup_time);
+    EXPECT_LT(ping_fetcher.context().monotonic_remote_transmit_time,
+              aos::monotonic_clock::now());
+
     EXPECT_EQ(ping_index, ping_fetcher->value());
     EXPECT_FALSE(ping_fetcher.FetchNext());
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   // Shut everyone else down.
-  StopPi1Client();
-  StopPi2Server();
+  pi1_.StopClient();
+  pi2_.StopServer();
   pi1_remote_timestamp_thread.reset();
-  StopPi1Server();
+  pi1_.StopServer();
 }
 
 // Test that differing config sha256's result in no connection.
@@ -1194,42 +1224,42 @@
   // hope for the best.  We can be more generous in the future if we need to.
   //
   // We are faking the application names by passing in --application_name=foo
-  OnPi1();
+  pi1_.OnPi();
 
-  MakePi1Server(
+  pi1_.MakeServer(
       "dummy sha256                                                    ");
-  MakePi1Client();
+  pi1_.MakeClient();
 
   // And build the app for testing.
-  MakePi1Test();
+  pi1_.MakeTest("test1", &pi2_);
   aos::Fetcher<ServerStatistics> pi1_server_statistics_fetcher =
-      pi1_test_event_loop->MakeFetcher<ServerStatistics>("/pi1/aos");
+      pi1_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi1/aos");
   aos::Fetcher<ClientStatistics> pi1_client_statistics_fetcher =
-      pi1_test_event_loop->MakeFetcher<ClientStatistics>("/pi1/aos");
+      pi1_.test_event_loop_->MakeFetcher<ClientStatistics>("/pi1/aos");
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
-  MakePi2Server();
+  pi2_.OnPi();
+  pi2_.MakeServer();
 
   // And build the app for testing.
-  MakePi2Test();
+  pi2_.MakeTest("test1", &pi1_);
   aos::Fetcher<ServerStatistics> pi2_server_statistics_fetcher =
-      pi2_test_event_loop->MakeFetcher<ServerStatistics>("/pi2/aos");
+      pi2_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi2/aos");
   aos::Fetcher<ClientStatistics> pi2_client_statistics_fetcher =
-      pi2_test_event_loop->MakeFetcher<ClientStatistics>("/pi2/aos");
+      pi2_.test_event_loop_->MakeFetcher<ClientStatistics>("/pi2/aos");
 
   // Wait until we are connected, then send.
 
-  StartPi1Test();
-  StartPi2Test();
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Server();
+  pi1_.StartTest();
+  pi2_.StartTest();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartServer();
 
   {
-    MakePi2Client();
+    pi2_.MakeClient();
 
-    RunPi2Client(chrono::milliseconds(3050));
+    pi2_.RunClient(chrono::milliseconds(3050));
 
     // Now confirm we are synchronized.
     EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
@@ -1270,15 +1300,15 @@
     VLOG(1) << aos::FlatbufferToJson(pi2_client_statistics_fetcher.get());
     VLOG(1) << aos::FlatbufferToJson(pi1_client_statistics_fetcher.get());
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   // Shut everyone else down.
-  StopPi1Server();
-  StopPi1Client();
-  StopPi2Server();
-  StopPi1Test();
-  StopPi2Test();
+  pi1_.StopServer();
+  pi1_.StopClient();
+  pi2_.StopServer();
+  pi1_.StopTest();
+  pi2_.StopTest();
 }
 
 // Test that a client which connects with too big a message gets disconnected
@@ -1298,54 +1328,54 @@
   // hope for the best.  We can be more generous in the future if we need to.
   //
   // We are faking the application names by passing in --application_name=foo
-  OnPi1();
+  pi1_.OnPi();
 
-  MakePi1Server();
-  MakePi1Client();
+  pi1_.MakeServer();
+  pi1_.MakeClient();
 
   // And build the app for testing.
-  MakePi1Test();
+  pi1_.MakeTest("test1", &pi2_);
   aos::Fetcher<ServerStatistics> pi1_server_statistics_fetcher =
-      pi1_test_event_loop->MakeFetcher<ServerStatistics>("/pi1/aos");
+      pi1_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi1/aos");
   aos::Fetcher<ClientStatistics> pi1_client_statistics_fetcher =
-      pi1_test_event_loop->MakeFetcher<ClientStatistics>("/pi1/aos");
+      pi1_.test_event_loop_->MakeFetcher<ClientStatistics>("/pi1/aos");
 
   // Now do it for "raspberrypi2", the client.
-  OnPi2();
-  MakePi2Server();
+  pi2_.OnPi();
+  pi2_.MakeServer();
 
   // And build the app for testing.
-  MakePi2Test();
+  pi2_.MakeTest("test1", &pi1_);
   aos::Fetcher<ServerStatistics> pi2_server_statistics_fetcher =
-      pi2_test_event_loop->MakeFetcher<ServerStatistics>("/pi2/aos");
+      pi2_.test_event_loop_->MakeFetcher<ServerStatistics>("/pi2/aos");
   aos::Fetcher<ClientStatistics> pi2_client_statistics_fetcher =
-      pi2_test_event_loop->MakeFetcher<ClientStatistics>("/pi2/aos");
+      pi2_.test_event_loop_->MakeFetcher<ClientStatistics>("/pi2/aos");
 
   // Wait until we are connected, then send.
 
-  StartPi1Test();
-  StartPi2Test();
-  StartPi1Server();
-  StartPi1Client();
-  StartPi2Server();
+  pi1_.StartTest();
+  pi2_.StartTest();
+  pi1_.StartServer();
+  pi1_.StartClient();
+  pi2_.StartServer();
 
   {
     // Now, spin up a SctpClient and send a massive hunk of data.  This should
     // trigger a disconnect, but no crash.
-    OnPi2();
+    pi2_.OnPi();
     FLAGS_application_name = "pi2_message_bridge_client";
-    pi2_client_event_loop =
-        std::make_unique<aos::ShmEventLoop>(&config.message());
-    pi2_client_event_loop->SetRuntimeRealtimePriority(1);
+    pi2_.client_event_loop_ =
+        std::make_unique<aos::ShmEventLoop>(&config_.message());
+    pi2_.client_event_loop_->SetRuntimeRealtimePriority(1);
 
-    const aos::Node *const remote_node = CHECK_NOTNULL(
-        configuration::GetNode(pi2_client_event_loop->configuration(), "pi1"));
+    const aos::Node *const remote_node = CHECK_NOTNULL(configuration::GetNode(
+        pi2_.client_event_loop_->configuration(), "pi1"));
 
     const aos::FlatbufferDetachedBuffer<aos::message_bridge::Connect>
         connect_message(MakeConnectMessage(
-            pi2_client_event_loop->configuration(),
-            pi2_client_event_loop->node(), "pi1",
-            pi2_client_event_loop->boot_uuid(), config_sha256));
+            pi2_.client_event_loop_->configuration(),
+            pi2_.client_event_loop_->node(), "pi1",
+            pi2_.client_event_loop_->boot_uuid(), config_sha256_));
 
     SctpClient client(remote_node->hostname()->string_view(),
                       remote_node->port(),
@@ -1369,20 +1399,21 @@
 
     const std::string big_data(kBigMessageSize, 'a');
 
-    pi2_client_event_loop->epoll()->OnReadable(client.fd(), [&]() {
+    pi2_.client_event_loop_->epoll()->OnReadable(client.fd(), [&]() {
       aos::unique_c_ptr<Message> message = client.Read();
       client.FreeMessage(std::move(message));
     });
 
-    aos::TimerHandler *const send_big_message = pi2_client_event_loop->AddTimer(
-        [&]() { CHECK(client.Send(kConnectStream(), big_data, 0)); });
+    aos::TimerHandler *const send_big_message =
+        pi2_.client_event_loop_->AddTimer(
+            [&]() { CHECK(client.Send(kConnectStream(), big_data, 0)); });
 
-    pi2_client_event_loop->OnRun([this, send_big_message]() {
-      send_big_message->Schedule(pi2_client_event_loop->monotonic_now() +
+    pi2_.client_event_loop_->OnRun([this, send_big_message]() {
+      send_big_message->Schedule(pi2_.client_event_loop_->monotonic_now() +
                                  chrono::seconds(1));
     });
 
-    RunPi2Client(chrono::milliseconds(3050));
+    pi2_.RunClient(chrono::milliseconds(3050));
 
     // Now confirm we are synchronized.
     EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
@@ -1417,17 +1448,17 @@
     VLOG(1) << aos::FlatbufferToJson(pi1_server_statistics_fetcher.get());
     VLOG(1) << aos::FlatbufferToJson(pi1_client_statistics_fetcher.get());
 
-    pi2_client_event_loop->epoll()->DeleteFd(client.fd());
+    pi2_.client_event_loop_->epoll()->DeleteFd(client.fd());
 
-    StopPi2Client();
+    pi2_.StopClient();
   }
 
   // Shut everyone else down.
-  StopPi1Server();
-  StopPi1Client();
-  StopPi2Server();
-  StopPi1Test();
-  StopPi2Test();
+  pi1_.StopServer();
+  pi1_.StopClient();
+  pi2_.StopServer();
+  pi1_.StopTest();
+  pi2_.StopTest();
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -1437,4 +1468,71 @@
               true},
         Param{"message_bridge_test_common_config.json", false}));
 
+// Tests the case in which the configurations for the server and client are
+// different - specifically the case where the client's config allows it to
+// "talk" to the server, while the server's config does not allow the client to
+// "talk" to it. The expectation in such a case is that we don't crash or raise
+// an exception.
+TEST(MessageBridgeTests, MismatchedServerAndClientConfigs) {
+  // Make a `MessageBridgeServer` with the config
+  // `message_bridge_test_mismatched_configs_pi1_and_pi3_config.json`.
+  // In this config, `pi1` talks to `pi3`, but does *not* talk to `pi2`.
+  PiNode pi1("pi1", "raspberrypi", "pi1_message_bridge_server",
+             "message_bridge_test_mismatched_configs_pi1_and_pi3_config.json");
+  pi1.OnPi();
+  pi1.MakeServer();
+  aos::ShmEventLoop pi1_test_event_loop(&pi1.config_.message());
+  aos::Fetcher<ServerStatistics> pi1_server_statistics_fetcher =
+      pi1_test_event_loop.MakeFetcher<ServerStatistics>("/pi1/aos");
+
+  // Make a `MessageBridgeClient` with the config
+  // `message_bridge_test_mismatched_configs_pi1_and_pi2_config.json`.
+  // In this config, `pi1` talks to `pi2`.
+  // Reasoning:
+  // Due to this mismatch between the configs of the server and client,
+  // when the client `pi2` sends a "connect" request to the server `pi1`,
+  // there will be no server node placed in the
+  // `MessageBridgeServerStatus::nodes_` vector at the index corresponding to
+  // the client node's index. In such a case, we expect to not crash or raise an
+  // exception.
+  PiNode pi2("pi2", "raspberrypi2", "pi2_message_bridge_client",
+             "message_bridge_test_mismatched_configs_pi1_and_pi2_config.json");
+  pi2.OnPi();
+  pi2.MakeClient();
+
+  // Put the server and client on 2 separate threaded runners and start running.
+  pi1.StartServer();
+  pi2.StartClient();
+
+  // Sleep here while the server and client threads run for 1 second.
+  // During this time, the client will attempt to connect to the server.
+  // We've set them up with mismatching configs such that the
+  // server does not expect to talk to the client, but the client does
+  // expect to connect to the server.
+  // We expect that neither of the threads crashes/raises an exception.
+  // If any of them does, the test terminates and the exception is reported
+  // via the stack trace when running the test.
+  std::this_thread::sleep_for(chrono::milliseconds(1000));
+
+  EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
+  // Since pi1's configuration is such that it expects to talk only to pi3,
+  // we expect the number of connections to be 1, and the node to
+  // be `pi3`.
+  EXPECT_EQ(pi1_server_statistics_fetcher->connections()->size(), 1);
+  const ServerConnection *const pi1_connection =
+      pi1_server_statistics_fetcher->connections()->Get(0);
+  EXPECT_EQ(pi1_connection->node()->name()->string_view(), "pi3");
+  // Since we didn't really spawn a `pi3` node in this test, we expect
+  // that the connection is disconnected, and the connection count is 0.
+  EXPECT_EQ(pi1_connection->state(), State::DISCONNECTED);
+  EXPECT_EQ(pi1_connection->connection_count(), 0u);
+  // Also, since no connection was established, we expect that there is
+  // no `connected_since_time` set.
+  EXPECT_FALSE(pi1_connection->has_connected_since_time());
+
+  // If we got here, everything went well. Stop the threads.
+  pi1.StopServer();
+  pi2.StopClient();
+}
+
 }  // namespace aos::message_bridge::testing
diff --git a/aos/network/message_bridge_test_lib.cc b/aos/network/message_bridge_test_lib.cc
index 0062bfd..6b8b49e 100644
--- a/aos/network/message_bridge_test_lib.cc
+++ b/aos/network/message_bridge_test_lib.cc
@@ -40,249 +40,159 @@
 }
 
 MessageBridgeParameterizedTest::MessageBridgeParameterizedTest()
-    : config(aos::configuration::ReadConfig(
-          ArtifactPath(absl::StrCat("aos/network/", GetParam().config)))),
-      config_sha256(Sha256(config.span())),
-      pi1_boot_uuid_(UUID::Random()),
-      pi2_boot_uuid_(UUID::Random()) {
+    : pi1_("pi1", "raspberrypi", "pi1_message_bridge_server",
+           GetParam().config),
+      pi2_("pi2", "raspberrypi2", "pi2_message_bridge_client",
+           GetParam().config),
+      config_(aos::configuration::ReadConfig(GetParam().config)),
+      config_sha256_(Sha256(config_.span())) {
   // Make sure that we clean up all the shared memory queues so that we cannot
   // inadvertently be influenced other tests or by previously run AOS
   // applications (in a fully sharded test running inside the bazel sandbox,
   // this should not matter).
-  util::UnlinkRecursive(ShmBase("pi1"));
-  util::UnlinkRecursive(ShmBase("pi2"));
+  util::UnlinkRecursive(ShmBase(pi1_.node_name_));
+  util::UnlinkRecursive(ShmBase(pi2_.node_name_));
 }
 
 bool MessageBridgeParameterizedTest::shared() const {
   return GetParam().shared;
 }
 
-void MessageBridgeParameterizedTest::OnPi1() {
-  DoSetShmBase("pi1");
-  FLAGS_override_hostname = "raspberrypi";
-  FLAGS_boot_uuid = pi1_boot_uuid_.ToString();
+PiNode::PiNode(const std::string node_name, const std::string host_name,
+               const std::string app_name, const std::string config_filename)
+    : boot_uuid_(UUID::Random()),
+      node_name_(node_name),
+      host_name_(host_name),
+      app_name_(app_name),
+      config_(aos::configuration::ReadConfig(config_filename)),
+      config_sha256_(Sha256(config_.span())) {}
+
+void PiNode::OnPi() {
+  DoSetShmBase(node_name_);
+  FLAGS_override_hostname = host_name_;
+  FLAGS_boot_uuid = boot_uuid_.ToString();
 }
 
-void MessageBridgeParameterizedTest::OnPi2() {
-  DoSetShmBase("pi2");
-  FLAGS_override_hostname = "raspberrypi2";
-  FLAGS_boot_uuid = pi2_boot_uuid_.ToString();
-}
-
-void MessageBridgeParameterizedTest::MakePi1Server(
-    std::string server_config_sha256) {
-  OnPi1();
-  LOG(INFO) << "Making pi1 server";
-  FLAGS_application_name = "pi1_message_bridge_server";
-  pi1_server_event_loop =
-      std::make_unique<aos::ShmEventLoop>(&config.message());
-  pi1_server_event_loop->SetRuntimeRealtimePriority(1);
-  pi1_message_bridge_server = std::make_unique<MessageBridgeServer>(
-      pi1_server_event_loop.get(),
-      server_config_sha256.size() == 0 ? config_sha256 : server_config_sha256,
+void PiNode::MakeServer(const std::string server_config_sha256) {
+  OnPi();
+  LOG(INFO) << "Making " << node_name_ << " server";
+  FLAGS_application_name = app_name_;
+  server_event_loop_ = std::make_unique<aos::ShmEventLoop>(&config_.message());
+  server_event_loop_->SetRuntimeRealtimePriority(1);
+  message_bridge_server_ = std::make_unique<MessageBridgeServer>(
+      server_event_loop_.get(),
+      server_config_sha256.size() == 0 ? config_sha256_ : server_config_sha256,
       SctpAuthMethod::kNoAuth);
 }
 
-void MessageBridgeParameterizedTest::RunPi1Server(
-    chrono::nanoseconds duration) {
-  LOG(INFO) << "Running pi1 server";
+void PiNode::RunServer(const chrono::nanoseconds duration) {
+  LOG(INFO) << "Running " << node_name_ << " server";
   // Set up a shutdown callback.
-  aos::TimerHandler *const quit = pi1_server_event_loop->AddTimer(
-      [this]() { pi1_server_event_loop->Exit(); });
-  pi1_server_event_loop->OnRun([this, quit, duration]() {
+  aos::TimerHandler *const quit =
+      server_event_loop_->AddTimer([this]() { server_event_loop_->Exit(); });
+  server_event_loop_->OnRun([this, quit, duration]() {
     // Stop between timestamps, not exactly on them.
-    quit->Schedule(pi1_server_event_loop->monotonic_now() + duration);
+    quit->Schedule(server_event_loop_->monotonic_now() + duration);
   });
 
-  pi1_server_event_loop->Run();
+  server_event_loop_->Run();
 }
 
-void MessageBridgeParameterizedTest::StartPi1Server() {
-  LOG(INFO) << "Starting pi1 server";
-  pi1_server_thread =
-      std::make_unique<ThreadedEventLoopRunner>(pi1_server_event_loop.get());
+void PiNode::StartServer() {
+  LOG(INFO) << "Starting " << node_name_ << " server";
+  server_thread_ =
+      std::make_unique<ThreadedEventLoopRunner>(server_event_loop_.get());
 }
 
-void MessageBridgeParameterizedTest::StopPi1Server() {
-  LOG(INFO) << "Stopping pi1 server";
-  pi1_server_thread.reset();
-  pi1_message_bridge_server.reset();
-  pi1_server_event_loop.reset();
+void PiNode::StopServer() {
+  LOG(INFO) << "Stopping " << node_name_ << " server";
+  server_thread_.reset();
+  message_bridge_server_.reset();
+  server_event_loop_.reset();
 }
 
-void MessageBridgeParameterizedTest::MakePi1Client() {
-  OnPi1();
-  LOG(INFO) << "Making pi1 client";
-  FLAGS_application_name = "pi1_message_bridge_client";
-  pi1_client_event_loop =
-      std::make_unique<aos::ShmEventLoop>(&config.message());
-  pi1_client_event_loop->SetRuntimeRealtimePriority(1);
-  pi1_message_bridge_client = std::make_unique<MessageBridgeClient>(
-      pi1_client_event_loop.get(), config_sha256, SctpAuthMethod::kNoAuth);
+void PiNode::MakeClient() {
+  OnPi();
+  LOG(INFO) << "Making " << node_name_ << " client";
+  FLAGS_application_name = app_name_;
+  client_event_loop_ = std::make_unique<aos::ShmEventLoop>(&config_.message());
+  client_event_loop_->SetRuntimeRealtimePriority(1);
+  message_bridge_client_ = std::make_unique<MessageBridgeClient>(
+      client_event_loop_.get(), config_sha256_, SctpAuthMethod::kNoAuth);
 }
 
-void MessageBridgeParameterizedTest::StartPi1Client() {
-  LOG(INFO) << "Starting pi1 client";
-  pi1_client_thread =
-      std::make_unique<ThreadedEventLoopRunner>(pi1_client_event_loop.get());
+void PiNode::StartClient() {
+  LOG(INFO) << "Starting " << node_name_ << " client";
+  client_thread_ =
+      std::make_unique<ThreadedEventLoopRunner>(client_event_loop_.get());
 }
 
-void MessageBridgeParameterizedTest::StopPi1Client() {
-  LOG(INFO) << "Stopping pi1 client";
-  pi1_client_thread.reset();
-  pi1_message_bridge_client.reset();
-  pi1_client_event_loop.reset();
+void PiNode::StopClient() {
+  LOG(INFO) << "Stopping " << node_name_ << " client";
+  client_thread_.reset();
+  message_bridge_client_.reset();
+  client_event_loop_.reset();
 }
 
-void MessageBridgeParameterizedTest::MakePi1Test() {
-  OnPi1();
-  LOG(INFO) << "Making pi1 test";
-  FLAGS_application_name = "test1";
-  pi1_test_event_loop = std::make_unique<aos::ShmEventLoop>(&config.message());
+void PiNode::MakeTest(const std::string test_app_name,
+                      const PiNode *other_node) {
+  OnPi();
+  LOG(INFO) << "Making " << node_name_ << " test";
+  FLAGS_application_name = test_app_name;
+  test_event_loop_ = std::make_unique<aos::ShmEventLoop>(&config_.message());
 
-  pi1_test_event_loop->MakeWatcher(
-      "/pi1/aos", [](const ServerStatistics &stats) {
-        VLOG(1) << "/pi1/aos ServerStatistics " << FlatbufferToJson(&stats);
+  std::string channel_name = "/" + node_name_ + "/aos";
+  test_event_loop_->MakeWatcher(
+      channel_name, [channel_name](const ServerStatistics &stats) {
+        VLOG(1) << channel_name << " ServerStatistics "
+                << FlatbufferToJson(&stats);
       });
 
-  pi1_test_event_loop->MakeWatcher(
-      "/pi1/aos", [](const ClientStatistics &stats) {
-        VLOG(1) << "/pi1/aos ClientStatistics " << FlatbufferToJson(&stats);
+  test_event_loop_->MakeWatcher(
+      channel_name, [channel_name](const ClientStatistics &stats) {
+        VLOG(1) << channel_name << " ClientStatistics "
+                << FlatbufferToJson(&stats);
       });
 
-  pi1_test_event_loop->MakeWatcher("/pi1/aos", [](const Timestamp &timestamp) {
-    VLOG(1) << "/pi1/aos Timestamp " << FlatbufferToJson(&timestamp);
-  });
-  pi1_test_event_loop->MakeWatcher("/pi2/aos", [this](
-                                                   const Timestamp &timestamp) {
-    VLOG(1) << "/pi2/aos Timestamp " << FlatbufferToJson(&timestamp);
-    EXPECT_EQ(pi1_test_event_loop->context().source_boot_uuid, pi2_boot_uuid_);
-  });
+  test_event_loop_->MakeWatcher(channel_name,
+                                [channel_name](const Timestamp &timestamp) {
+                                  VLOG(1) << channel_name << " Timestamp "
+                                          << FlatbufferToJson(&timestamp);
+                                });
+  std::string other_channel_name = "/" + other_node->node_name_ + "/aos";
+  test_event_loop_->MakeWatcher(
+      other_channel_name,
+      [this, other_channel_name, other_node](const Timestamp &timestamp) {
+        VLOG(1) << other_channel_name << " Timestamp "
+                << FlatbufferToJson(&timestamp);
+        EXPECT_EQ(test_event_loop_->context().source_boot_uuid,
+                  other_node->boot_uuid_);
+      });
 }
 
-void MessageBridgeParameterizedTest::StartPi1Test() {
-  LOG(INFO) << "Starting pi1 test";
-  pi1_test_thread =
-      std::make_unique<ThreadedEventLoopRunner>(pi1_test_event_loop.get());
+void PiNode::StartTest() {
+  LOG(INFO) << "Starting " << node_name_ << " test";
+  test_thread_ =
+      std::make_unique<ThreadedEventLoopRunner>(test_event_loop_.get());
 }
 
-void MessageBridgeParameterizedTest::StopPi1Test() {
-  LOG(INFO) << "Stopping pi1 test";
-  pi1_test_thread.reset();
+void PiNode::StopTest() {
+  LOG(INFO) << "Stopping " << node_name_ << " test";
+  test_thread_.reset();
 }
 
-void MessageBridgeParameterizedTest::MakePi2Server() {
-  OnPi2();
-  LOG(INFO) << "Making pi2 server";
-  FLAGS_application_name = "pi2_message_bridge_server";
-  pi2_server_event_loop =
-      std::make_unique<aos::ShmEventLoop>(&config.message());
-  pi2_server_event_loop->SetRuntimeRealtimePriority(1);
-  pi2_message_bridge_server = std::make_unique<MessageBridgeServer>(
-      pi2_server_event_loop.get(), config_sha256, SctpAuthMethod::kNoAuth);
-}
-
-void MessageBridgeParameterizedTest::RunPi2Server(
-    chrono::nanoseconds duration) {
-  LOG(INFO) << "Running pi2 server";
-  // Schedule a shutdown callback.
-  aos::TimerHandler *const quit = pi2_server_event_loop->AddTimer(
-      [this]() { pi2_server_event_loop->Exit(); });
-  pi2_server_event_loop->OnRun([this, quit, duration]() {
-    // Stop between timestamps, not exactly on them.
-    quit->Schedule(pi2_server_event_loop->monotonic_now() + duration);
-  });
-
-  pi2_server_event_loop->Run();
-}
-
-void MessageBridgeParameterizedTest::StartPi2Server() {
-  LOG(INFO) << "Starting pi2 server";
-  pi2_server_thread =
-      std::make_unique<ThreadedEventLoopRunner>(pi2_server_event_loop.get());
-}
-
-void MessageBridgeParameterizedTest::StopPi2Server() {
-  LOG(INFO) << "Stopping pi2 server";
-  pi2_server_thread.reset();
-  pi2_message_bridge_server.reset();
-  pi2_server_event_loop.reset();
-}
-
-void MessageBridgeParameterizedTest::MakePi2Client() {
-  OnPi2();
-  LOG(INFO) << "Making pi2 client";
-  FLAGS_application_name = "pi2_message_bridge_client";
-  pi2_client_event_loop =
-      std::make_unique<aos::ShmEventLoop>(&config.message());
-  pi2_client_event_loop->SetRuntimeRealtimePriority(1);
-  pi2_message_bridge_client = std::make_unique<MessageBridgeClient>(
-      pi2_client_event_loop.get(), config_sha256, SctpAuthMethod::kNoAuth);
-}
-
-void MessageBridgeParameterizedTest::RunPi2Client(
-    chrono::nanoseconds duration) {
+void PiNode::RunClient(const chrono::nanoseconds duration) {
   LOG(INFO) << "Running pi2 client";
   // Run for 5 seconds to make sure we have time to estimate the offset.
-  aos::TimerHandler *const quit = pi2_client_event_loop->AddTimer(
-      [this]() { pi2_client_event_loop->Exit(); });
-  pi2_client_event_loop->OnRun([this, quit, duration]() {
+  aos::TimerHandler *const quit =
+      client_event_loop_->AddTimer([this]() { client_event_loop_->Exit(); });
+  client_event_loop_->OnRun([this, quit, duration]() {
     // Stop between timestamps, not exactly on them.
-    quit->Schedule(pi2_client_event_loop->monotonic_now() + duration);
+    quit->Schedule(client_event_loop_->monotonic_now() + duration);
   });
 
   // And go!
-  pi2_client_event_loop->Run();
-}
-
-void MessageBridgeParameterizedTest::StartPi2Client() {
-  LOG(INFO) << "Starting pi2 client";
-  pi2_client_thread =
-      std::make_unique<ThreadedEventLoopRunner>(pi2_client_event_loop.get());
-}
-
-void MessageBridgeParameterizedTest::StopPi2Client() {
-  LOG(INFO) << "Stopping pi2 client";
-  pi2_client_thread.reset();
-  pi2_message_bridge_client.reset();
-  pi2_client_event_loop.reset();
-}
-
-void MessageBridgeParameterizedTest::MakePi2Test() {
-  OnPi2();
-  LOG(INFO) << "Making pi2 test";
-  FLAGS_application_name = "test2";
-  pi2_test_event_loop = std::make_unique<aos::ShmEventLoop>(&config.message());
-
-  pi2_test_event_loop->MakeWatcher(
-      "/pi2/aos", [](const ServerStatistics &stats) {
-        VLOG(1) << "/pi2/aos ServerStatistics " << FlatbufferToJson(&stats);
-      });
-
-  pi2_test_event_loop->MakeWatcher(
-      "/pi2/aos", [](const ClientStatistics &stats) {
-        VLOG(1) << "/pi2/aos ClientStatistics " << FlatbufferToJson(&stats);
-      });
-
-  pi2_test_event_loop->MakeWatcher("/pi1/aos", [this](
-                                                   const Timestamp &timestamp) {
-    VLOG(1) << "/pi1/aos Timestamp " << FlatbufferToJson(&timestamp);
-    EXPECT_EQ(pi2_test_event_loop->context().source_boot_uuid, pi1_boot_uuid_);
-  });
-  pi2_test_event_loop->MakeWatcher("/pi2/aos", [](const Timestamp &timestamp) {
-    VLOG(1) << "/pi2/aos Timestamp " << FlatbufferToJson(&timestamp);
-  });
-}
-
-void MessageBridgeParameterizedTest::StartPi2Test() {
-  LOG(INFO) << "Starting pi2 test";
-  pi2_test_thread =
-      std::make_unique<ThreadedEventLoopRunner>(pi2_test_event_loop.get());
-}
-
-void MessageBridgeParameterizedTest::StopPi2Test() {
-  LOG(INFO) << "Stopping pi2 test";
-  pi2_test_thread.reset();
+  client_event_loop_->Run();
 }
 
 }  // namespace aos::message_bridge::testing
diff --git a/aos/network/message_bridge_test_lib.h b/aos/network/message_bridge_test_lib.h
index 047cff5..f8c0a3a 100644
--- a/aos/network/message_bridge_test_lib.h
+++ b/aos/network/message_bridge_test_lib.h
@@ -46,6 +46,45 @@
   bool shared;
 };
 
+class PiNode {
+ public:
+  PiNode(const std::string node_name, const std::string host_name,
+         const std::string app_name, const std::string config_filename);
+  // OnPi* sets the global state necessary to pretend that a ShmEventLoop is on
+  // the requisite system.
+  void OnPi();
+  void MakeServer(const std::string server_config_sha256 = "");
+  void RunServer(const chrono::nanoseconds duration);
+  void RunClient(const chrono::nanoseconds duration);
+  void StartServer();
+  void StopServer();
+  void MakeClient();
+  void StartClient();
+  void StopClient();
+  void MakeTest(const std::string test_app_name, const PiNode *other_node);
+  void StartTest();
+  void StopTest();
+
+  const UUID boot_uuid_;
+  const std::string node_name_;
+  const std::string host_name_;
+  const std::string app_name_;
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  std::string config_sha256_;
+
+  std::unique_ptr<aos::ShmEventLoop> server_event_loop_;
+  std::unique_ptr<MessageBridgeServer> message_bridge_server_;
+  std::unique_ptr<ThreadedEventLoopRunner> server_thread_;
+
+  std::unique_ptr<aos::ShmEventLoop> client_event_loop_;
+  std::unique_ptr<MessageBridgeClient> message_bridge_client_;
+  std::unique_ptr<ThreadedEventLoopRunner> client_thread_;
+
+  std::unique_ptr<aos::ShmEventLoop> test_event_loop_;
+  std::unique_ptr<ThreadedEventLoopRunner> test_thread_;
+};
+
 class MessageBridgeParameterizedTest
     : public ::testing::TestWithParam<struct Param> {
  protected:
@@ -53,83 +92,13 @@
 
   bool shared() const;
 
-  // OnPi* sets the global state necessary to pretend that a ShmEventLoop is on
-  // the requisite system.
-  void OnPi1();
-
-  void OnPi2();
-
-  void MakePi1Server(std::string server_config_sha256 = "");
-
-  void RunPi1Server(chrono::nanoseconds duration);
-
-  void StartPi1Server();
-
-  void StopPi1Server();
-
-  void MakePi1Client();
-
-  void StartPi1Client();
-
-  void StopPi1Client();
-
-  void MakePi1Test();
-
-  void StartPi1Test();
-
-  void StopPi1Test();
-
-  void MakePi2Server();
-
-  void RunPi2Server(chrono::nanoseconds duration);
-
-  void StartPi2Server();
-
-  void StopPi2Server();
-
-  void MakePi2Client();
-
-  void RunPi2Client(chrono::nanoseconds duration);
-
-  void StartPi2Client();
-
-  void StopPi2Client();
-
-  void MakePi2Test();
-
-  void StartPi2Test();
-
-  void StopPi2Test();
-
   gflags::FlagSaver flag_saver_;
 
-  aos::FlatbufferDetachedBuffer<aos::Configuration> config;
-  std::string config_sha256;
+  PiNode pi1_;
+  PiNode pi2_;
 
-  const UUID pi1_boot_uuid_;
-  const UUID pi2_boot_uuid_;
-
-  std::unique_ptr<aos::ShmEventLoop> pi1_server_event_loop;
-  std::unique_ptr<MessageBridgeServer> pi1_message_bridge_server;
-  std::unique_ptr<ThreadedEventLoopRunner> pi1_server_thread;
-
-  std::unique_ptr<aos::ShmEventLoop> pi1_client_event_loop;
-  std::unique_ptr<MessageBridgeClient> pi1_message_bridge_client;
-  std::unique_ptr<ThreadedEventLoopRunner> pi1_client_thread;
-
-  std::unique_ptr<aos::ShmEventLoop> pi1_test_event_loop;
-  std::unique_ptr<ThreadedEventLoopRunner> pi1_test_thread;
-
-  std::unique_ptr<aos::ShmEventLoop> pi2_server_event_loop;
-  std::unique_ptr<MessageBridgeServer> pi2_message_bridge_server;
-  std::unique_ptr<ThreadedEventLoopRunner> pi2_server_thread;
-
-  std::unique_ptr<aos::ShmEventLoop> pi2_client_event_loop;
-  std::unique_ptr<MessageBridgeClient> pi2_message_bridge_client;
-  std::unique_ptr<ThreadedEventLoopRunner> pi2_client_thread;
-
-  std::unique_ptr<aos::ShmEventLoop> pi2_test_event_loop;
-  std::unique_ptr<ThreadedEventLoopRunner> pi2_test_thread;
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  std::string config_sha256_;
 };
 
 }  // namespace aos::message_bridge::testing
diff --git a/aos/network/message_bridge_test_mismatched_configs_pi1_and_pi2.json b/aos/network/message_bridge_test_mismatched_configs_pi1_and_pi2.json
new file mode 100644
index 0000000..bad48c8
--- /dev/null
+++ b/aos/network/message_bridge_test_mismatched_configs_pi1_and_pi2.json
@@ -0,0 +1,137 @@
+{
+    "channels": [
+      {
+        "name": "/pi1/aos",
+        "type": "aos.logging.LogMessageFbs",
+        "source_node": "pi1",
+        "frequency": 200,
+        "num_senders": 20,
+        "max_size": 2048
+      },
+      {
+        "name": "/pi2/aos",
+        "type": "aos.logging.LogMessageFbs",
+        "source_node": "pi2",
+        "frequency": 200,
+        "num_senders": 20,
+        "max_size": 2048
+      },
+      {
+        "name": "/pi1/aos",
+        "type": "aos.message_bridge.Timestamp",
+        "source_node": "pi1",
+        "frequency": 15,
+        "max_size": 200,
+        "destination_nodes": [
+          {
+            "name": "pi2",
+            "priority": 1,
+            "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+            "timestamp_logger_nodes": ["pi1"],
+            "time_to_live": 5000000
+          }
+        ]
+      },
+      {
+        "name": "/pi2/aos",
+        "type": "aos.message_bridge.Timestamp",
+        "source_node": "pi2",
+        "frequency": 15,
+        "max_size": 200,
+        "destination_nodes": [
+          {
+            "name": "pi1",
+            "priority": 1,
+            "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+            "timestamp_logger_nodes": ["pi2"],
+            "time_to_live": 5000000
+          }
+        ]
+      },
+      {
+        "name": "/pi1/aos",
+        "type": "aos.message_bridge.ServerStatistics",
+        "source_node": "pi1",
+        "frequency": 2
+      },
+      {
+        "name": "/pi2/aos",
+        "type": "aos.message_bridge.ServerStatistics",
+        "source_node": "pi2",
+        "frequency": 2
+      },
+      {
+        "name": "/pi1/aos",
+        "type": "aos.message_bridge.ClientStatistics",
+        "source_node": "pi1",
+        "frequency": 15
+      },
+      {
+        "name": "/pi2/aos",
+        "type": "aos.message_bridge.ClientStatistics",
+        "source_node": "pi2",
+        "frequency": 15
+      },
+      {
+        "name": "/pi1/aos/remote_timestamps/pi2",
+        "type": "aos.message_bridge.RemoteMessage",
+        "source_node": "pi1"
+      },
+      {
+        "name": "/pi2/aos/remote_timestamps/pi1",
+        "type": "aos.message_bridge.RemoteMessage",
+        "source_node": "pi2",
+        "frequency": 15
+      },
+      {
+        "name": "/pi1/aos",
+        "type": "aos.timing.Report",
+        "source_node": "pi1",
+        "frequency": 50,
+        "num_senders": 20,
+        "max_size": 2048
+      },
+      {
+        "name": "/pi2/aos",
+        "type": "aos.timing.Report",
+        "source_node": "pi2",
+        "frequency": 50,
+        "num_senders": 20,
+        "max_size": 2048
+      }
+    ],
+    "maps": [
+      {
+        "match": {
+          "name": "/aos*",
+          "source_node": "pi1"
+        },
+        "rename": {
+          "name": "/pi1/aos"
+        }
+      },
+      {
+        "match": {
+          "name": "/aos*",
+          "source_node": "pi2"
+        },
+        "rename": {
+          "name": "/pi2/aos"
+        }
+      }
+    ],
+    "nodes": [
+      {
+        "name": "pi1",
+        "hostname": "localhost",
+        "hostnames": ["raspberrypi"],
+        "port": 9971
+      },
+      {
+        "name": "pi2",
+        "hostname": "localhost",
+        "hostnames": ["raspberrypi2"],
+        "port": 9972
+      }
+    ]
+  }
diff --git a/aos/network/message_bridge_test_mismatched_configs_pi1_and_pi3.json b/aos/network/message_bridge_test_mismatched_configs_pi1_and_pi3.json
new file mode 100644
index 0000000..20b3f9a
--- /dev/null
+++ b/aos/network/message_bridge_test_mismatched_configs_pi1_and_pi3.json
@@ -0,0 +1,152 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi3/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi3",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi3",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi3/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi3",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi3"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2
+    },
+    {
+      "name": "/pi3/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi3",
+      "frequency": 2
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi3/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi3",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi3",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi3/aos/remote_timestamps/pi1",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "pi3",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi3/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi3",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi3"
+      },
+      "rename": {
+        "name": "/pi3/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1",
+      "hostname": "localhost",
+      "hostnames": ["raspberrypi"],
+      "port": 9971
+    },
+    {
+      "name": "pi2",
+      "hostname": "localhost",
+      "hostnames": ["raspberrypi2"],
+      "port": 9972
+    },
+    {
+      "name": "pi3",
+      "hostname": "localhost",
+      "hostnames": ["raspberrypi3"],
+      "port": 9973
+    }
+  ]
+}
diff --git a/aos/network/multinode_timestamp_filter.cc b/aos/network/multinode_timestamp_filter.cc
index 0a0385e..cae9e1f 100644
--- a/aos/network/multinode_timestamp_filter.cc
+++ b/aos/network/multinode_timestamp_filter.cc
@@ -82,7 +82,7 @@
 }
 }  // namespace
 
-size_t NewtonSolver::solve_number_ = 0u;
+std::atomic<size_t> NewtonSolver::solve_number_ = 0u;
 
 NewtonSolver::NewtonSolver() : my_solve_number_(solve_number_++) {}
 
@@ -2016,10 +2016,21 @@
               CHECK_NOTNULL(filter);
               const Node *node = configuration()->nodes()->Get(node_index);
 
+              // The remote time could be from a reliable message long ago,
+              // whereas the transmit time is the latest timestamp we have on
+              // the far side.  Use that for recovering time when we have it.
+              // There are a ton of logs from before that timestamp was added.
+              // Fall back to the remote time in those.
+              const BootTimestamp monotonic_remote_time =
+                  msg->monotonic_remote_transmit_time.time !=
+                          monotonic_clock::min_time
+                      ? msg->monotonic_remote_transmit_time
+                      : msg->monotonic_remote_time;
+
               // Call the correct method depending on if we are the forward or
               // reverse direction here.
               filter->Sample(node, msg->monotonic_event_time,
-                             msg->monotonic_remote_time);
+                             monotonic_remote_time);
 
               if (!node_samples_.empty()) {
                 const size_t sending_node_index =
@@ -2029,8 +2040,8 @@
                 // and monotonic_event_time was the time it was received.
                 node_samples_[node_index]
                     .nodes[sending_node_index]
-                    .messages.emplace(std::make_pair(
-                        msg->monotonic_event_time, msg->monotonic_remote_time));
+                    .messages.emplace(std::make_pair(msg->monotonic_event_time,
+                                                     monotonic_remote_time));
               }
 
               if (msg->monotonic_timestamp_time != BootTimestamp::min_time()) {
@@ -2044,7 +2055,7 @@
                   const size_t sending_node_index =
                       source_node_index_[msg->channel_index];
                   // The timestamp then went back from node node_index to
-                  // sending_node_index.  monotonic_event_time is the time it
+                  // sending_node_index.  monotonic_transmit_time is the time it
                   // was sent, and monotonic_timestamp_time was the time it was
                   // received.
                   node_samples_[sending_node_index]
@@ -2933,7 +2944,8 @@
     }
 
     if (VLOG_IS_ON(1)) {
-      VLOG(1) << "Candidate solution for node " << node_a_index << " is";
+      VLOG(1) << "Candidate solution for node " << node_a_index
+              << " on solve number " << solver.my_solve_number() << " is";
       for (size_t i = 0; i < solution.size(); ++i) {
         VLOG(1) << "  " << solution[i];
       }
diff --git a/aos/network/multinode_timestamp_filter.h b/aos/network/multinode_timestamp_filter.h
index 893f816..66dcbff 100644
--- a/aos/network/multinode_timestamp_filter.h
+++ b/aos/network/multinode_timestamp_filter.h
@@ -1,6 +1,7 @@
 #ifndef AOS_NETWORK_MULTINODE_TIMESTAMP_FILTER_H_
 #define AOS_NETWORK_MULTINODE_TIMESTAMP_FILTER_H_
 
+#include <atomic>
 #include <functional>
 #include <map>
 #include <string_view>
@@ -171,7 +172,7 @@
   size_t my_solve_number_;
 
   // The global solve number counter used to deterministically find problems.
-  static size_t solve_number_;
+  static std::atomic<size_t> solve_number_;
 };
 
 // A condensed representation of the time estimation problem statement.  This is
diff --git a/aos/network/remote_data.fbs b/aos/network/remote_data.fbs
index 0813e57..4e4c7af 100644
--- a/aos/network/remote_data.fbs
+++ b/aos/network/remote_data.fbs
@@ -20,6 +20,12 @@
 
   // UUID for this boot.  This is 16 bytes long.
   boot_uuid:[uint8] (id: 5);
+
+  // Time that the message was handed to the kernel to be published over the
+  // network on the remote node.
+  //
+  // See MessageHeader fbs definition for more details.
+  monotonic_remote_transmit_time:int64 = -9223372036854775808(id: 6);
 }
 
 root_type RemoteData;
diff --git a/aos/network/remote_message.fbs b/aos/network/remote_message.fbs
index 6d2a8d1..4305f51 100644
--- a/aos/network/remote_message.fbs
+++ b/aos/network/remote_message.fbs
@@ -32,6 +32,13 @@
 
   // UUID for this boot.
   boot_uuid:[uint8] (id: 9);
+
+  // The time that the message was transmitted on the source node to the
+  // destination node i.e handed to the kernel to be published over the
+  // message bridge.
+  //
+  // See MessageHeader fbs definition for more details.
+  monotonic_remote_transmit_time:int64 = -9223372036854775808(id: 10);
 }
 
 root_type RemoteMessage;
diff --git a/aos/network/timestamp_filter.cc b/aos/network/timestamp_filter.cc
index 914f993..f3aea62 100644
--- a/aos/network/timestamp_filter.cc
+++ b/aos/network/timestamp_filter.cc
@@ -25,6 +25,12 @@
   return ss.str();
 }
 
+std::string TimeString(const logger::BootTimestamp t, logger::BootDuration o) {
+  std::stringstream ss;
+  ss << "O(" << t << ") = " << o << ", remote " << t + o;
+  return ss.str();
+}
+
 std::string TimeString(const aos::monotonic_clock::time_point t_base, double t,
                        std::chrono::nanoseconds o_base, double o) {
   std::stringstream ss;
@@ -43,6 +49,11 @@
   return TimeString(std::get<0>(t), std::get<1>(t));
 }
 
+std::string TimeString(
+    const std::tuple<logger::BootTimestamp, logger::BootDuration> t) {
+  return TimeString(std::get<0>(t), std::get<1>(t));
+}
+
 void ClippedAverageFilterPrintHeader(FILE *fp) {
   fprintf(fp,
           "# time_since_start, sample_ns, filtered_offset, offset, "
@@ -531,6 +542,11 @@
     return std::make_pair(pointer, std::make_pair(t0, t1));
   }
 
+  VLOG(1) << "Other points are: " << pointer.other_points_.size();
+  for (const auto &x : pointer.other_points_) {
+    VLOG(1) << "  " << TimeString(x.second);
+  }
+
   // The invariant of pointer is that other_points is bounded by t0, t1. Confirm
   // it before we return things depending on it since it is easy.
   CHECK_GT(std::get<0>(pointer.other_points_[0].second), std::get<0>(t0));
@@ -1266,36 +1282,39 @@
 void NoncausalTimestampFilter::Sample(BootTimestamp monotonic_now_all,
                                       BootDuration sample_ns) {
   filter(monotonic_now_all.boot, sample_ns.boot)
-      ->filter.Sample(monotonic_now_all.time, sample_ns.duration);
+      ->filter.Sample(monotonic_now_all, sample_ns);
 }
 
 void NoncausalTimestampFilter::SingleFilter::Sample(
-    monotonic_clock::time_point monotonic_now, chrono::nanoseconds sample_ns) {
+    logger::BootTimestamp monotonic_now, logger::BootDuration sample_ns) {
   // The first sample is easy.  Just do it!
   if (timestamps_.size() == 0) {
     VLOG(1) << node_names_ << " Initial sample of "
             << TimeString(monotonic_now, sample_ns);
-    timestamps_.emplace_back(std::make_tuple(monotonic_now, sample_ns));
+    timestamps_.emplace_back(
+        std::make_tuple(monotonic_now.time, sample_ns.duration));
     CHECK(!fully_frozen_)
         << ": " << node_names_
         << " Returned a horizontal line previously and then "
            "got a new sample at "
         << monotonic_now << ", "
-        << chrono::duration<double>(monotonic_now - std::get<0>(timestamps_[0]))
+        << chrono::duration<double>(monotonic_now.time -
+                                    std::get<0>(timestamps_[0]))
                .count()
         << " seconds after the last sample at " << std::get<0>(timestamps_[0])
         << ".  Increase --time_estimation_buffer_seconds to greater than "
-        << chrono::duration<double>(monotonic_now - std::get<0>(timestamps_[0]))
+        << chrono::duration<double>(monotonic_now.time -
+                                    std::get<0>(timestamps_[0]))
                .count()
         << ", or set --force_timestamp_loading";
     return;
   }
-  CHECK_GT(monotonic_now, frozen_time_)
+  CHECK_GT(monotonic_now.time, frozen_time_)
       << ": " << node_names_ << " Tried to insert " << monotonic_now
       << " before the frozen time of " << frozen_time_
       << ".  Increase "
          "--time_estimation_buffer_seconds to greater than "
-      << chrono::duration<double>(frozen_time_ - monotonic_now).count()
+      << chrono::duration<double>(frozen_time_ - monotonic_now.time).count()
       << ", or set --force_timestamp_loading";
 
   // Future samples get quite a bit harder.  We want the line to track the
@@ -1303,12 +1322,13 @@
   std::tuple<aos::monotonic_clock::time_point, chrono::nanoseconds> back =
       timestamps_.back();
 
-  aos::monotonic_clock::duration dt = monotonic_now - std::get<0>(back);
-  aos::monotonic_clock::duration doffset = sample_ns - std::get<1>(back);
+  aos::monotonic_clock::duration dt = monotonic_now.time - std::get<0>(back);
+  aos::monotonic_clock::duration doffset =
+      sample_ns.duration - std::get<1>(back);
 
   if (dt == chrono::nanoseconds(0) && doffset == chrono::nanoseconds(0)) {
     VLOG(1) << node_names_ << " Duplicate sample of O(" << monotonic_now
-            << ") = " << sample_ns.count() << ", remote time "
+            << ") = " << sample_ns << ", remote time "
             << monotonic_now + sample_ns;
 
     return;
@@ -1337,11 +1357,13 @@
         << " Returned a horizontal line previously and then got a new "
            "sample at "
         << monotonic_now << ", "
-        << chrono::duration<double>(monotonic_now - std::get<0>(timestamps_[0]))
+        << chrono::duration<double>(monotonic_now.time -
+                                    std::get<0>(timestamps_[0]))
                .count()
         << " seconds after the last sample at " << std::get<0>(timestamps_[0])
         << ".  Increase --time_estimation_buffer_seconds to greater than "
-        << chrono::duration<double>(monotonic_now - std::get<0>(timestamps_[0]))
+        << chrono::duration<double>(monotonic_now.time -
+                                    std::get<0>(timestamps_[0]))
                .count()
         << ", or set --force_timestamp_loading";
 
@@ -1364,10 +1386,12 @@
           << ": " << node_names_ << " Can't pop an already frozen sample "
           << TimeString(back) << " while inserting "
           << TimeString(monotonic_now, sample_ns) << ", "
-          << chrono::duration<double>(monotonic_now - std::get<0>(back)).count()
+          << chrono::duration<double>(monotonic_now.time - std::get<0>(back))
+                 .count()
           << " seconds in the past.  Increase --time_estimation_buffer_seconds "
              "to greater than "
-          << chrono::duration<double>(monotonic_now - std::get<0>(back)).count()
+          << chrono::duration<double>(monotonic_now.time - std::get<0>(back))
+                 .count()
           << ", or set --force_timestamp_loading";
       VLOG(1) << node_names_
               << " Removing now invalid sample during back propegation of "
@@ -1375,13 +1399,14 @@
       timestamps_.pop_back();
 
       back = timestamps_.back();
-      dt = monotonic_now - std::get<0>(back);
-      doffset = sample_ns - std::get<1>(back);
+      dt = monotonic_now.time - std::get<0>(back);
+      doffset = sample_ns.duration - std::get<1>(back);
     }
 
     VLOG(1) << node_names_ << " Added sample of "
             << TimeString(monotonic_now, sample_ns);
-    timestamps_.emplace_back(std::make_tuple(monotonic_now, sample_ns));
+    timestamps_.emplace_back(
+        std::make_tuple(monotonic_now.time, sample_ns.duration));
     return;
   }
 
@@ -1389,7 +1414,7 @@
   // point.  lower_bound returns the element which we are supposed to insert
   // "before".
   auto it = std::lower_bound(
-      timestamps_.begin(), timestamps_.end(), monotonic_now,
+      timestamps_.begin(), timestamps_.end(), monotonic_now.time,
       [](const std::tuple<aos::monotonic_clock::time_point,
                           std::chrono::nanoseconds>
              x,
@@ -1403,9 +1428,9 @@
   if (it == timestamps_.begin()) {
     // We are being asked to add at the beginning.
     {
-      const chrono::nanoseconds dt = std::get<0>(*it) - monotonic_now;
+      const chrono::nanoseconds dt = std::get<0>(*it) - monotonic_now.time;
       const chrono::nanoseconds original_offset = std::get<1>(*it);
-      const chrono::nanoseconds doffset = original_offset - sample_ns;
+      const chrono::nanoseconds doffset = original_offset - sample_ns.duration;
 
       if (dt == chrono::nanoseconds(0) && doffset >= chrono::nanoseconds(0)) {
         VLOG(1) << node_names_ << " Redundant timestamp "
@@ -1418,15 +1443,18 @@
 
     VLOG(1) << node_names_ << " Added sample at beginning "
             << TimeString(monotonic_now, sample_ns);
-    timestamps_.insert(it, std::make_tuple(monotonic_now, sample_ns));
+    timestamps_.insert(it,
+                       std::make_tuple(monotonic_now.time, sample_ns.duration));
 
     while (true) {
       // First point was too positive, so we need to remove points after it
       // until we are valid.
       auto second = timestamps_.begin() + 1;
       if (second != timestamps_.end()) {
-        const chrono::nanoseconds dt = std::get<0>(*second) - monotonic_now;
-        const chrono::nanoseconds doffset = std::get<1>(*second) - sample_ns;
+        const chrono::nanoseconds dt =
+            std::get<0>(*second) - monotonic_now.time;
+        const chrono::nanoseconds doffset =
+            std::get<1>(*second) - sample_ns.duration;
 
         if (absl::int128(doffset.count()) *
                 absl::int128(MaxVelocityRatio::den) <
@@ -1477,10 +1505,12 @@
             << " < " << monotonic_now << " < " << std::get<0>(*it);
 
     {
-      chrono::nanoseconds prior_dt = monotonic_now - std::get<0>(*(it - 1));
-      chrono::nanoseconds prior_doffset = sample_ns - std::get<1>(*(it - 1));
-      chrono::nanoseconds next_dt = std::get<0>(*it) - monotonic_now;
-      chrono::nanoseconds next_doffset = std::get<1>(*it) - sample_ns;
+      chrono::nanoseconds prior_dt =
+          monotonic_now.time - std::get<0>(*(it - 1));
+      chrono::nanoseconds prior_doffset =
+          sample_ns.duration - std::get<1>(*(it - 1));
+      chrono::nanoseconds next_dt = std::get<0>(*it) - monotonic_now.time;
+      chrono::nanoseconds next_doffset = std::get<1>(*it) - sample_ns.duration;
 
       // If we are worse than either the previous or next point, discard.
       if (absl::int128(prior_doffset.count()) *
@@ -1517,8 +1547,8 @@
     // Now, insert and start propagating forwards and backwards anything we've
     // made invalid.  Do this simultaneously so we keep discovering anything
     // new.
-    auto middle_it =
-        timestamps_.insert(it, std::make_tuple(monotonic_now, sample_ns));
+    auto middle_it = timestamps_.insert(
+        it, std::make_tuple(monotonic_now.time, sample_ns.duration));
     VLOG(1) << node_names_ << " Inserted " << TimeString(*middle_it);
 
     while (middle_it != timestamps_.end() && middle_it != timestamps_.begin()) {
@@ -1602,20 +1632,16 @@
       removed = true;
     }
 
-    if (timestamps_size == 2) {
+    if (timestamps_size <= 2) {
       if (pop_filter_ + 1u >= filters_.size()) {
         return removed;
       }
 
-      // There is 1 more filter, see if there is enough data in it to switch
-      // over to it.
-      if (filters_[pop_filter_ + 1]->filter.timestamps_size() < 2u) {
-        return removed;
-      }
       if (time <
-          BootTimestamp{.boot = static_cast<size_t>(boot_filter->boot.first),
+          BootTimestamp{.boot = static_cast<size_t>(
+                            filters_[pop_filter_ + 1]->boot.first),
                         .time = std::get<0>(
-                            filters_[pop_filter_ + 1]->filter.timestamp(1))}) {
+                            filters_[pop_filter_ + 1]->filter.timestamp(0))}) {
         return removed;
       }
     }
@@ -1661,16 +1687,80 @@
   return next_to_consume_ + 1 < timestamps_.size();
 }
 
+std::optional<std::tuple<logger::BootTimestamp, logger::BootDuration>>
+NoncausalTimestampFilter::Observe() const {
+  if (filters_.size() == 0u) {
+    return std::nullopt;
+  }
+
+  size_t current_filter = std::max(static_cast<ssize_t>(0), current_filter_);
+  while (true) {
+    const BootFilter &filter = *filters_[current_filter];
+    std::optional<
+        std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds>>
+        result = filter.filter.Observe();
+    if (!result) {
+      if (current_filter + 1 == filters_.size()) {
+        return std::nullopt;
+      } else {
+        ++current_filter;
+        continue;
+      }
+    }
+    auto final_result = std::make_tuple(
+        logger::BootTimestamp{static_cast<size_t>(filter.boot.first),
+                              std::get<0>(*result)},
+        logger::BootDuration{static_cast<size_t>(filter.boot.second),
+                             std::get<1>(*result)});
+    VLOG(1) << NodeNames() << " Observed sample of "
+            << TimeString(final_result);
+    return final_result;
+  }
+}
+
 std::optional<std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds>>
 NoncausalTimestampFilter::SingleFilter::Observe() const {
   if (timestamps_.empty() || next_to_consume_ >= timestamps_.size()) {
     return std::nullopt;
   }
-  VLOG(1) << node_names_ << " Observed sample of "
+  VLOG(2) << node_names_ << " Observed sample of "
           << TimeString(timestamp(next_to_consume_));
   return timestamp(next_to_consume_);
 }
 
+std::optional<std::tuple<logger::BootTimestamp, logger::BootDuration>>
+NoncausalTimestampFilter::Consume() {
+  if (filters_.size() == 0u) {
+    return std::nullopt;
+  }
+  DCHECK_LT(current_filter_, static_cast<ssize_t>(filters_.size()));
+
+  while (true) {
+    std::optional<
+        std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds>>
+        result =
+            current_filter_ < 0 ? std::nullopt
+                                : filters_[current_filter_]->filter.Consume();
+    if (!result) {
+      if (static_cast<size_t>(current_filter_ + 1) == filters_.size()) {
+        return std::nullopt;
+      } else {
+        ++current_filter_;
+        continue;
+      }
+    }
+    BootFilter &filter = *filters_[current_filter_];
+    auto final_result = std::make_tuple(
+        logger::BootTimestamp{static_cast<size_t>(filter.boot.first),
+                              std::get<0>(*result)},
+        logger::BootDuration{static_cast<size_t>(filter.boot.second),
+                             std::get<1>(*result)});
+    VLOG(1) << NodeNames() << " Consumed sample of "
+            << TimeString(final_result);
+    return final_result;
+  }
+}
+
 std::optional<std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds>>
 NoncausalTimestampFilter::SingleFilter::Consume() {
   if (timestamps_.empty() || next_to_consume_ >= timestamps_.size()) {
@@ -1678,7 +1768,7 @@
   }
 
   auto result = timestamp(next_to_consume_);
-  VLOG(1) << node_names_ << " Consumed sample of " << TimeString(result);
+  VLOG(2) << node_names_ << " Consumed sample of " << TimeString(result);
   ++next_to_consume_;
   return result;
 }
diff --git a/aos/network/timestamp_filter.h b/aos/network/timestamp_filter.h
index be9318e..2361797 100644
--- a/aos/network/timestamp_filter.h
+++ b/aos/network/timestamp_filter.h
@@ -469,63 +469,11 @@
   // because solving for them doesn't add any additional value.  We will already
   // be solving the other direction.
   std::optional<std::tuple<logger::BootTimestamp, logger::BootDuration>>
-  Observe() const {
-    if (filters_.size() == 0u) {
-      return std::nullopt;
-    }
-
-    size_t current_filter = std::max(static_cast<ssize_t>(0), current_filter_);
-    while (true) {
-      const BootFilter &filter = *filters_[current_filter];
-      std::optional<
-          std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds>>
-          result = filter.filter.Observe();
-      if (!result) {
-        if (current_filter + 1 == filters_.size()) {
-          return std::nullopt;
-        } else {
-          ++current_filter;
-          continue;
-        }
-      }
-      return std::make_tuple(
-          logger::BootTimestamp{static_cast<size_t>(filter.boot.first),
-                                std::get<0>(*result)},
-          logger::BootDuration{static_cast<size_t>(filter.boot.second),
-                               std::get<1>(*result)});
-    }
-  }
+  Observe() const;
   // Returns the next timestamp in the queue if available, incrementing the
   // pointer.
   std::optional<std::tuple<logger::BootTimestamp, logger::BootDuration>>
-  Consume() {
-    if (filters_.size() == 0u) {
-      return std::nullopt;
-    }
-    DCHECK_LT(current_filter_, static_cast<ssize_t>(filters_.size()));
-
-    while (true) {
-      std::optional<
-          std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds>>
-          result =
-              current_filter_ < 0 ? std::nullopt
-                                  : filters_[current_filter_]->filter.Consume();
-      if (!result) {
-        if (static_cast<size_t>(current_filter_ + 1) == filters_.size()) {
-          return std::nullopt;
-        } else {
-          ++current_filter_;
-          continue;
-        }
-      }
-      BootFilter &filter = *filters_[current_filter_];
-      return std::make_tuple(
-          logger::BootTimestamp{static_cast<size_t>(filter.boot.first),
-                                std::get<0>(*result)},
-          logger::BootDuration{static_cast<size_t>(filter.boot.second),
-                               std::get<1>(*result)});
-    }
-  }
+  Consume();
 
   // Public for testing.
   // Assuming that there are at least 2 points in timestamps_, finds the 2
@@ -736,8 +684,8 @@
                           aos::monotonic_clock::time_point tb_base, double tb,
                           bool validate_popped, bool quiet) const;
 
-    void Sample(monotonic_clock::time_point monotonic_now,
-                std::chrono::nanoseconds sample_ns);
+    void Sample(logger::BootTimestamp monotonic_now,
+                logger::BootDuration sample_ns);
 
    private:
     std::string node_names_;
diff --git a/aos/network/timestamp_filter_test.cc b/aos/network/timestamp_filter_test.cc
index f3b7290..0c37ed5 100644
--- a/aos/network/timestamp_filter_test.cc
+++ b/aos/network/timestamp_filter_test.cc
@@ -797,9 +797,10 @@
     filter.FreezeUntil(tb, {0, monotonic_clock::min_time});
 
     EXPECT_DEATH({ filter.Sample(tb, oa); },
-                 "monotonic_now > frozen_time_ \\(0.100000000sec vs. "
+                 "monotonic_now.time > frozen_time_ \\(0.100000000sec vs. "
                  "0.100000000sec\\) : test_a -> test_b Tried to insert "
-                 "0.100000000sec before the frozen time of 0.100000000sec.  "
+                 "\\{.boot=0, .time=0.100000000sec\\} before the frozen time "
+                 "of 0.100000000sec.  "
                  "Increase --time_estimation_buffer_seconds to greater than 0");
   }
 
@@ -816,7 +817,8 @@
         { filter.Sample(tc, oc); },
         "test_a -> test_b Returned a horizontal line previously and then got a "
         "new sample at "
-        "0.200000000sec, 0.2 seconds after the last sample at 0.000000000sec");
+        "\\{.boot=0, .time=0.200000000sec\\}, 0.2 seconds after the last "
+        "sample at 0.000000000sec");
   }
 
   {
@@ -830,9 +832,10 @@
 
     EXPECT_DEATH(
         { filter.Sample(tb, ob); },
-        "monotonic_now > frozen_time_ \\(0.100000000sec vs. "
+        "monotonic_now.time > frozen_time_ \\(0.100000000sec vs. "
         "0.200000000sec\\) : test_a -> test_b Tried to insert "
-        "0.100000000sec before the frozen time of 0.200000000sec.  "
+        "\\{.boot=0, .time=0.100000000sec\\} before the frozen time of "
+        "0.200000000sec.  "
         "Increase --time_estimation_buffer_seconds to greater than 0.1");
   }
 
@@ -848,9 +851,10 @@
     filter.FreezeUntil(tb, {0, monotonic_clock::min_time});
 
     EXPECT_DEATH({ filter.Sample(tb, oa); },
-                 "monotonic_now > frozen_time_ \\(0.100000000sec vs. "
+                 "monotonic_now.time > frozen_time_ \\(0.100000000sec vs. "
                  "0.100000000sec\\) : test_a -> test_b Tried to insert "
-                 "0.100000000sec before the frozen time of 0.100000000sec.  "
+                 "\\{.boot=0, .time=0.100000000sec\\} before the frozen time "
+                 "of 0.100000000sec.  "
                  "Increase --time_estimation_buffer_seconds to greater than 0");
     EXPECT_DEATH({ filter.Sample(tb + chrono::nanoseconds(1), oa); },
                  "test_a -> test_b Can't pop an already frozen sample");
diff --git a/aos/realtime.cc b/aos/realtime.cc
index 72daa6b..1e10457 100644
--- a/aos/realtime.cc
+++ b/aos/realtime.cc
@@ -161,8 +161,24 @@
   MarkRealtime(false);
 }
 
+std::ostream &operator<<(std::ostream &stream, const cpu_set_t &cpuset) {
+  stream << "{CPUs ";
+  bool first_found = false;
+  for (int i = 0; i < CPU_SETSIZE; ++i) {
+    if (CPU_ISSET(i, &cpuset)) {
+      if (first_found) {
+        stream << ", ";
+      }
+      stream << i;
+      first_found = true;
+    }
+  }
+  stream << "}";
+  return stream;
+}
+
 void SetCurrentThreadAffinity(const cpu_set_t &cpuset) {
-  PCHECK(sched_setaffinity(0, sizeof(cpuset), &cpuset) == 0);
+  PCHECK(sched_setaffinity(0, sizeof(cpuset), &cpuset) == 0) << cpuset;
 }
 
 void SetCurrentThreadName(const std::string_view name) {
diff --git a/aos/realtime.h b/aos/realtime.h
index caf0daa..261eb26 100644
--- a/aos/realtime.h
+++ b/aos/realtime.h
@@ -3,6 +3,7 @@
 
 #include <sched.h>
 
+#include <ostream>
 #include <string_view>
 
 #include "glog/logging.h"
@@ -28,6 +29,9 @@
 // name can have a maximum of 16 characters.
 void SetCurrentThreadName(const std::string_view name);
 
+// Stringifies the cpu_set_t for streams.
+std::ostream &operator<<(std::ostream &stream, const cpu_set_t &cpuset);
+
 // Creates a cpu_set_t from a list of CPUs.
 inline cpu_set_t MakeCpusetFromCpus(std::initializer_list<int> cpus) {
   cpu_set_t result;
diff --git a/aos/realtime_test.cc b/aos/realtime_test.cc
index 348489b..9a0c94e 100644
--- a/aos/realtime_test.cc
+++ b/aos/realtime_test.cc
@@ -132,6 +132,20 @@
 
 #endif
 
+// Tests that we see which CPUs we tried to set when it fails. This can be
+// useful for debugging.
+TEST(RealtimeDeathTest, SetAffinityErrorMessage) {
+  EXPECT_DEATH({ SetCurrentThreadAffinity(MakeCpusetFromCpus({1000})); },
+               "sched_setaffinity\\(0, sizeof\\(cpuset\\), &cpuset\\) == 0 "
+               "\\{CPUs 1000\\}: Invalid argument");
+  EXPECT_DEATH(
+      {
+        SetCurrentThreadAffinity(MakeCpusetFromCpus({1000, 1001}));
+      },
+      "sched_setaffinity\\(0, sizeof\\(cpuset\\), &cpuset\\) == 0 "
+      "\\{CPUs 1000, 1001\\}: Invalid argument");
+}
+
 }  // namespace aos::testing
 
 // We need a special gtest main to force die_on_malloc support on.  Otherwise
diff --git a/aos/starter/starter_rpc_lib.cc b/aos/starter/starter_rpc_lib.cc
index 891355e..754fc2c 100644
--- a/aos/starter/starter_rpc_lib.cc
+++ b/aos/starter/starter_rpc_lib.cc
@@ -63,6 +63,7 @@
     : event_loop_(event_loop),
       timeout_timer_(event_loop_->AddTimer([this]() { Timeout(); })),
       cmd_sender_(event_loop_->MakeSender<StarterRpc>("/aos")) {
+  timeout_timer_->set_name("rpc_timeout");
   if (configuration::MultiNode(event_loop_->configuration())) {
     for (const aos::Node *node :
          configuration::GetNodes(event_loop_->configuration())) {
diff --git a/aos/starter/starter_rpc_lib.h b/aos/starter/starter_rpc_lib.h
index ae2450c..a949928 100644
--- a/aos/starter/starter_rpc_lib.h
+++ b/aos/starter/starter_rpc_lib.h
@@ -37,8 +37,11 @@
     timeout_handler_ = handler;
   }
 
+  // Sets the callback to be called on success.  Note: this isn't safe to call
+  // while the previous success handler is running unless you use std::ref to
+  // manage its lifetime some other way.
   void SetSuccessHandler(std::function<void()> handler) {
-    success_handler_ = handler;
+    success_handler_ = std::move(handler);
   }
 
  private:
diff --git a/aos/starter/starter_test.cc b/aos/starter/starter_test.cc
index d26f414..5888233 100644
--- a/aos/starter/starter_test.cc
+++ b/aos/starter/starter_test.cc
@@ -535,7 +535,7 @@
       LOG(INFO) << "Waiting for starter to close.";
       std::this_thread::sleep_for(std::chrono::seconds(1));
     }
-    client.SetTimeoutHandler(stage3);
+    client.SetTimeoutHandler(std::ref(stage3));
     client.SetSuccessHandler([]() {
       LOG(INFO) << "stage3 success handler called.";
       FAIL() << ": Command should not have succeeded here.";
@@ -549,7 +549,7 @@
     LOG(INFO) << "Begin stage1";
     client.SetTimeoutHandler(
         []() { FAIL() << ": Command should not have timed out."; });
-    client.SetSuccessHandler(stage2);
+    client.SetSuccessHandler(std::ref(stage2));
     client.SendCommands({{Command::STOP, "ping", {client_node}}},
                         std::chrono::seconds(5));
     LOG(INFO) << "End stage1";
diff --git a/aos/starter/starterd_lib.cc b/aos/starter/starterd_lib.cc
index 38d519d..6a1d3ed 100644
--- a/aos/starter/starterd_lib.cc
+++ b/aos/starter/starterd_lib.cc
@@ -58,6 +58,8 @@
       top_(&event_loop_) {
   event_loop_.SkipAosLog();
 
+  cleanup_timer_->set_name("cleanup");
+
   if (!aos::configuration::MultiNode(config_msg_)) {
     event_loop_.MakeWatcher(
         "/aos",
diff --git a/aos/starter/subprocess.cc b/aos/starter/subprocess.cc
index d677922..07057a0 100644
--- a/aos/starter/subprocess.cc
+++ b/aos/starter/subprocess.cc
@@ -237,9 +237,16 @@
           event_loop_->AddTimer([this]() { MaybeHandleSignal(); })),
       on_change_({on_change}),
       quiet_flag_(quiet_flag) {
+  // Keep the length of the timer name bounded to some reasonable length.
+  start_timer_->set_name(absl::StrCat("app_start_", name.substr(0, 10)));
+  restart_timer_->set_name(absl::StrCat("app_restart_", name.substr(0, 10)));
+  stop_timer_->set_name(absl::StrCat("app_stop_", name.substr(0, 10)));
+  pipe_timer_->set_name(absl::StrCat("app_pipe_", name.substr(0, 10)));
+  child_status_handler_->set_name(
+      absl::StrCat("app_status_handler_", name.substr(0, 10)));
   // Every second poll to check if the child is dead. This is used as a
-  // default for the case where the user is not directly catching SIGCHLD and
-  // calling MaybeHandleSignal for us.
+  // default for the case where the user is not directly catching SIGCHLD
+  // and calling MaybeHandleSignal for us.
   child_status_handler_->Schedule(event_loop_->monotonic_now(),
                                   std::chrono::seconds(1));
 }
diff --git a/tools/python/mirror_pip_packages.py b/tools/python/mirror_pip_packages.py
index 76a33a8..0581054 100644
--- a/tools/python/mirror_pip_packages.py
+++ b/tools/python/mirror_pip_packages.py
@@ -20,7 +20,7 @@
 import requests
 from pkginfo import Wheel
 
-PLAT = "manylinux_2_31"
+PLAT = "manylinux_2_34"
 ARCH = "x86_64"
 WHEELHOUSE_MIRROR_URL = "https://software.frc971.org/Build-Dependencies/wheelhouse"
 PY_DEPS_WWWW_DIR = "/var/www/html/files/frc971/Build-Dependencies/wheelhouse"
diff --git a/tools/rust/defs.bzl b/tools/rust/defs.bzl
index 06f1ad1..0093184 100644
--- a/tools/rust/defs.bzl
+++ b/tools/rust/defs.bzl
@@ -8,12 +8,13 @@
     _rust_test = "rust_test",
 )
 
-def rust_doc_test(target_compatible_with = ["//tools/platforms/rust:has_support"], tags = [], **kwargs):
+def rust_doc_test(tags = [], **kwargs):
     # TODO(james): Attempting to execute this remotely results
     # in complaints about overly large files.
     _rust_doc_test(
         tags = tags + ["no-remote-exec"],
-        target_compatible_with = target_compatible_with,
+        # TODO(adam.snaider): Investigate why doctests only work on x86_64.
+        target_compatible_with = ["@platforms//cpu:x86_64"],
         **kwargs
     )