diff --git a/example/brunel/brunel_miniapp.cpp b/example/brunel/brunel_miniapp.cpp
index 2041b7bf13b5c752c6e925b4e895417c0e98a6c6..1c83d2bcd5449e72d95c6ba6fa953845f4f3696a 100644
--- a/example/brunel/brunel_miniapp.cpp
+++ b/example/brunel/brunel_miniapp.cpp
@@ -1,6 +1,7 @@
 #include <cmath>
 #include <exception>
 #include <fstream>
+#include <iomanip>
 #include <iostream>
 #include <memory>
 #include <set>
@@ -24,7 +25,6 @@
 #include <sup/ioutil.hpp>
 #include <sup/json_meter.hpp>
 #include <sup/path.hpp>
-#include <sup/spike_emitter.hpp>
 #include <sup/strsub.hpp>
 
 #ifdef ARB_MPI_ENABLED
@@ -216,7 +216,12 @@ int main(int argc, char** argv) {
         meters.start(context);
 
         // read parameters
-        io::cl_options options = io::read_options(argc, argv, root);
+        io::cl_options options = io::read_options(argc, argv);
+
+        std::fstream spike_out;
+        if (options.spike_file_output && root) {
+            spike_out = sup::open_or_throw("./spikes.gdf", std::ios_base::out, false);
+        }
 
         meters.checkpoint("setup", context);
 
@@ -257,31 +262,29 @@ int main(int argc, char** argv) {
 
         simulation sim(recipe, decomp, context);
 
-        // Initialize the spike exporting interface
-        std::fstream spike_out;
-        if (options.spike_file_output) {
-            using std::ios_base;
-
-            sup::path p = options.output_path;
-            p /= sup::strsub("%_%.%", options.file_name, rank, options.file_extension);
-
-            if (options.single_file_per_rank) {
-                spike_out = sup::open_or_throw(p, ios_base::out, !options.over_write);
-                sim.set_local_spike_callback(sup::spike_emitter(spike_out));
-            }
-            else if (root) {
-                spike_out = sup::open_or_throw(p, ios_base::out, !options.over_write);
-                sim.set_global_spike_callback(sup::spike_emitter(spike_out));
-            }
+        // Set up spike recording.
+        std::vector<arb::spike> recorded_spikes;
+        if (spike_out) {
+            sim.set_global_spike_callback([&recorded_spikes](auto& spikes) {
+                    recorded_spikes.insert(recorded_spikes.end(), spikes.begin(), spikes.end());
+                });
         }
 
         meters.checkpoint("model-init", context);
 
-        // run simulation
+        // Run simulation.
         sim.run(options.tfinal, options.dt);
 
         meters.checkpoint("model-simulate", context);
 
+        // Output spikes if requested.
+        if (spike_out) {
+            spike_out << std::fixed << std::setprecision(4);
+            for (auto& s: recorded_spikes) {
+                spike_out << s.source.gid << ' ' << s.time << '\n';
+            }
+        }
+
         // output profile and diagnostic feedback
         std::cout << profile::profiler_summary() << "\n";
         std::cout << "\nThere were " << sim.num_spikes() << " spikes\n";
diff --git a/example/brunel/io.cpp b/example/brunel/io.cpp
index 540cc559526f6181a58f53a6ffce09b8bca88513..72b6ca815bc29869342b7fe29b0599d05013e1de 100644
--- a/example/brunel/io.cpp
+++ b/example/brunel/io.cpp
@@ -79,9 +79,8 @@ namespace io {
     }
 
     // Read options from (optional) json file and command line arguments.
-    cl_options read_options(int argc, char** argv, bool allow_write) {
+    cl_options read_options(int argc, char** argv) {
         cl_options options;
-        std::string save_file = "";
 
         // Parse command line arguments.
         try {
diff --git a/example/brunel/io.hpp b/example/brunel/io.hpp
index 370e02acf6b8918ba5c6e43623ad500385ca4fc3..28c76bdb28ad605b069925c1b2fb699636230882 100644
--- a/example/brunel/io.hpp
+++ b/example/brunel/io.hpp
@@ -30,11 +30,6 @@ namespace io {
 
         // Parameters for spike output.
         bool spike_file_output = false;
-        bool single_file_per_rank = false;
-        bool over_write = true;
-        std::string output_path = "./";
-        std::string file_name = "spikes";
-        std::string file_extension = "gdf";
 
         // Turn on/off profiling output for all ranks.
         bool profile_only_zero = false;
@@ -57,5 +52,5 @@ namespace io {
 
     std::ostream& operator<<(std::ostream& o, const cl_options& opt);
 
-    cl_options read_options(int argc, char** argv, bool allow_write = true);
+    cl_options read_options(int argc, char** argv);
 } // namespace io
diff --git a/example/miniapp/io.cpp b/example/miniapp/io.cpp
index ac171ca503601471dd482a7c145c4d9ad851d5e4..4d8b19700086810030abccf7cc44282f25380a2e 100644
--- a/example/miniapp/io.cpp
+++ b/example/miniapp/io.cpp
@@ -229,13 +229,11 @@ cl_options read_options(int argc, char** argv, bool allow_write) {
 
                     // Parameters for spike output
                     update_option(options.spike_file_output, fopts, "spike_file_output");
-                    if (options.spike_file_output) {
-                        update_option(options.single_file_per_rank, fopts, "single_file_per_rank");
-                        update_option(options.over_write, fopts, "over_write");
-                        update_option(options.output_path, fopts, "output_path");
-                        update_option(options.file_name, fopts, "file_name");
-                        update_option(options.file_extension, fopts, "file_extension");
-                    }
+                    update_option(options.single_file_per_rank, fopts, "single_file_per_rank");
+                    update_option(options.over_write, fopts, "over_write");
+                    update_option(options.output_path, fopts, "output_path");
+                    update_option(options.file_name, fopts, "file_name");
+                    update_option(options.file_extension, fopts, "file_extension");
 
                     update_option(options.dry_run_ranks, fopts, "dry_run_ranks");
                 }
diff --git a/example/miniapp/miniapp.cpp b/example/miniapp/miniapp.cpp
index 5fe1cc5fc5934904e050beffa9dba5c8d1d8db2d..f4d744403d5f743e477c454b5373796edee2daa8 100644
--- a/example/miniapp/miniapp.cpp
+++ b/example/miniapp/miniapp.cpp
@@ -1,5 +1,6 @@
 #include <cmath>
 #include <exception>
+#include <iomanip>
 #include <iostream>
 #include <fstream>
 #include <memory>
@@ -23,7 +24,6 @@
 #include <sup/ioutil.hpp>
 #include <sup/json_meter.hpp>
 #include <sup/path.hpp>
-#include <sup/spike_emitter.hpp>
 #include <sup/strsub.hpp>
 
 #ifdef ARB_MPI_ENABLED
@@ -62,7 +62,8 @@ int main(int argc, char** argv) {
         arbenv::with_mpi guard(argc, argv, false);
         resources.gpu_id = arbenv::find_private_gpu(MPI_COMM_WORLD);
         auto context = arb::make_context(resources, MPI_COMM_WORLD);
-        root = arb::rank(context) == 0;
+        rank = arb::rank(context);
+        root = rank == 0;
 #else
         resources.gpu_id = arbenv::default_gpu();
         auto context = arb::make_context(resources);
@@ -85,6 +86,14 @@ int main(int argc, char** argv) {
         // threading back end, and 1 gpu if available.
         banner(context);
 
+        // Set up spike output if requested.
+        std::fstream spike_out;
+        if (options.spike_file_output && (root || options.single_file_per_rank)) {
+            sup::path p = options.output_path;
+            p /= sup::strsub("%_%.%", options.file_name, rank, options.file_extension);
+            spike_out = sup::open_or_throw(p, std::ios_base::out, !options.over_write);
+        }
+
         meters.checkpoint("setup", context);
 
         // determine what to attach probes to
@@ -130,37 +139,42 @@ int main(int argc, char** argv) {
 
         sim.set_binning_policy(binning_policy, options.bin_dt);
 
-        // Initialize the spike exporting interface
-        std::fstream spike_out;
-        if (options.spike_file_output) {
-            using std::ios_base;
-
-            sup::path p = options.output_path;
-            p /= sup::strsub("%_%.%", options.file_name, rank, options.file_extension);
+        // Set up spike recording.
+        std::vector<arb::spike> recorded_spikes;
+        if (spike_out) {
+            auto spike_callback = [&recorded_spikes](auto& spikes) {
+                recorded_spikes.insert(recorded_spikes.end(), spikes.begin(), spikes.end());
+            };
 
             if (options.single_file_per_rank) {
-                spike_out = sup::open_or_throw(p, ios_base::out, !options.over_write);
-                sim.set_local_spike_callback(sup::spike_emitter(spike_out));
+                sim.set_local_spike_callback(spike_callback);
             }
-            else if (rank==0) {
-                spike_out = sup::open_or_throw(p, ios_base::out, !options.over_write);
-                sim.set_global_spike_callback(sup::spike_emitter(spike_out));
+            else {
+                sim.set_global_spike_callback(spike_callback);
             }
         }
 
         meters.checkpoint("model-init", context);
 
-        // run model
+        // Run model.
         sim.run(options.tfinal, options.dt);
 
         meters.checkpoint("model-simulate", context);
 
-        // output profile and diagnostic feedback
+        // Output profile and diagnostic feedback.
         auto profile = profile::profiler_summary();
         std::cout << profile << "\n";
         std::cout << "\nthere were " << sim.num_spikes() << " spikes\n";
 
-        // save traces
+        // Save spikes.
+        if (spike_out) {
+            spike_out << std::fixed << std::setprecision(4);
+            for (auto& s: recorded_spikes) {
+                spike_out << s.source.gid << ' ' << s.time << '\n';
+            }
+        }
+
+        // Save traces.
         auto write_trace = options.trace_format=="json"? write_trace_json: write_trace_csv;
         for (const auto& trace: sample_traces) {
             write_trace(trace, options.trace_prefix);
diff --git a/sup/CMakeLists.txt b/sup/CMakeLists.txt
index 9e014f07681e8d693cb83a8a4b7b0b0c1760f281..227a760b3379e5b6ab26c0657ed62680684a492c 100644
--- a/sup/CMakeLists.txt
+++ b/sup/CMakeLists.txt
@@ -3,7 +3,6 @@ set(sup-sources
     ioutil.cpp
     json_meter.cpp
     path.cpp
-    spike_emitter.cpp
 )
 
 add_library(arbor-sup ${sup-sources})
diff --git a/sup/include/sup/spike_emitter.hpp b/sup/include/sup/spike_emitter.hpp
deleted file mode 100644
index 0656f00b3117f308c37b57dc56f701df4fb6a318..0000000000000000000000000000000000000000
--- a/sup/include/sup/spike_emitter.hpp
+++ /dev/null
@@ -1,16 +0,0 @@
-#include <functional>
-#include <iosfwd>
-#include <vector>
-
-#include <arbor/spike.hpp>
-
-namespace sup {
-
-struct spike_emitter {
-    std::reference_wrapper<std::ostream> out;
-
-    spike_emitter(std::ostream& out);
-    void operator()(const std::vector<arb::spike>&);
-};
-
-} // namespace sup
diff --git a/sup/spike_emitter.cpp b/sup/spike_emitter.cpp
deleted file mode 100644
index ec451be1a8daa2c939671e010bef15e4e515769c..0000000000000000000000000000000000000000
--- a/sup/spike_emitter.cpp
+++ /dev/null
@@ -1,23 +0,0 @@
-#include <functional>
-#include <iostream>
-
-#include <arbor/spike.hpp>
-#include <sup/spike_emitter.hpp>
-
-namespace sup {
-
-spike_emitter::spike_emitter(std::ostream& out): out(out) {}
-
-void spike_emitter::operator()(const std::vector<arb::spike>& spikes) {
-    char line[45];
-    for (auto& s: spikes) {
-        int n = std::snprintf(line, sizeof(line), "%u %.4f",  s.source.gid, s.time);
-        if (n<0) {
-            throw std::system_error(errno, std::generic_category());
-        }
-
-        out.get().write(line, n).put('\n');
-    }
-};
-
-} // namespace sup
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index b89cece7d93a51260b005155aa18cd37948d85aa..439a1034ffa11e848d4fe29c432582a4509ac7f4 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -99,7 +99,6 @@ set(unit_sources
     test_span.cpp
     test_spikes.cpp
     test_spike_store.cpp
-    test_spike_emitter.cpp
     test_stats.cpp
     test_strprintf.cpp
     test_swcio.cpp
diff --git a/test/unit/test_spike_emitter.cpp b/test/unit/test_spike_emitter.cpp
deleted file mode 100644
index a66812773861296398c2b3f75c35e4d421eb3fe3..0000000000000000000000000000000000000000
--- a/test/unit/test_spike_emitter.cpp
+++ /dev/null
@@ -1,30 +0,0 @@
-#include "../gtest.h"
-
-#include <sstream>
-#include <string>
-#include <vector>
-
-#include <arbor/spike.hpp>
-#include <sup/spike_emitter.hpp>
-
-TEST(spike_emitter, formatting) {
-    std::stringstream out;
-    auto callback = sup::spike_emitter(out);
-
-    std::vector<arb::spike> spikes = {
-        { { 0, 0 }, 0.0 },
-        { { 0, 0 }, 0.1 },
-        { { 1, 0 }, 1.0 },
-        { { 1, 0 }, 1.1 }
-    };
-
-    callback(spikes);
-
-    std::string expected =
-        "0 0.0000\n"
-        "0 0.1000\n"
-        "1 1.0000\n"
-        "1 1.1000\n";
-
-    EXPECT_EQ(expected, out.str());
-}