diff --git a/src/DynareMain.cc b/src/DynareMain.cc
index f2d6182bb56e6c66d2b52324a5990ba595cd5b83..aa7677ea45b3bd971b19620efe0d196426349eb4 100644
--- a/src/DynareMain.cc
+++ b/src/DynareMain.cc
@@ -524,6 +524,11 @@ main(int argc, char **argv)
                            nointeractive, config_file, check_model_changes, minimal_workspace, compute_xrefs,
                            mexext, matlabroot, dynareroot, onlymodel, gui, notime);
 
+  /* Not technically needed since those are std::jthread, but ensures that the
+     preprocessor final message is printed after the end of compilation (and is
+     not printed in case of compilation failure). */
+  ModelTree::joinMEXCompilationThreads();
+
   cout << "Preprocessing completed." << endl;
   return EXIT_SUCCESS;
 }
diff --git a/src/Makefile.am b/src/Makefile.am
index 45d6d7e54dd26e1e990d300e41112410761541eb..36c5ff56db6cc8752d89753f5375e46e71bdf864 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -70,7 +70,8 @@ EXTRA_DIST = \
 # The -I. is for <FlexLexer.h>
 dynare_preprocessor_CPPFLAGS = $(BOOST_CPPFLAGS) -I.
 dynare_preprocessor_LDFLAGS = $(AM_LDFLAGS) $(BOOST_LDFLAGS)
-dynare_preprocessor_LDADD = macro/libmacro.a
+# -lpthread is no longer necessary for glibc ⩾ 2.34 (i.e. Debian “bookworm” 12)
+dynare_preprocessor_LDADD = macro/libmacro.a -lpthread
 
 # -Ca flag comes from hitting a hard-coded size limit.
 # Partial explanation: https://www.owlfolio.org/possibly-useful/flex-input-scanner-rules-are-too-complicated
diff --git a/src/ModelTree.cc b/src/ModelTree.cc
index affb101a9e12da322ccea3eca143281610890e2e..364a1777cabab273b98c1d2e7ce6212449a9c5f3 100644
--- a/src/ModelTree.cc
+++ b/src/ModelTree.cc
@@ -36,6 +36,8 @@
 #include <regex>
 #include <utility>
 
+vector<jthread> ModelTree::mex_compilation_threads {};
+
 void
 ModelTree::copyHelper(const ModelTree &m)
 {
@@ -1751,11 +1753,18 @@ ModelTree::compileMEX(const string &basename, const string &funcname, const stri
 
   cout << "Compiling " << funcname << " MEX..." << endl << cmd.str() << endl;
 
-  if (system(cmd.str().c_str()))
-    {
-      cerr << "Compilation failed" << endl;
-      exit(EXIT_FAILURE);
-    }
+  /* The command line must be captured by value by the thread (a reference
+     would quickly become dangling). And std::ostringstream is not copyable, so
+     capture a std::string. */
+  string cmd_str { cmd.str() };
+  mex_compilation_threads.emplace_back([cmd_str]
+  {
+    if (system(cmd_str.c_str()))
+      {
+        cerr << "Compilation failed" << endl;
+        exit(EXIT_FAILURE);
+      }
+  });
 }
 
 void
@@ -1858,3 +1867,10 @@ ModelTree::writeBlockBytecodeAdditionalDerivatives([[maybe_unused]] BytecodeWrit
                                                    [[maybe_unused]] const deriv_node_temp_terms_t &tef_terms) const
 {
 }
+
+void
+ModelTree::joinMEXCompilationThreads()
+{
+  for (auto &it : mex_compilation_threads)
+    it.join();
+}
diff --git a/src/ModelTree.hh b/src/ModelTree.hh
index 9cea1fbfc6f50f5838549f21dfd1d42567ecb893..eba02be259931c7e21ec17c5fc9ffe05d911f6a2 100644
--- a/src/ModelTree.hh
+++ b/src/ModelTree.hh
@@ -29,6 +29,7 @@
 #include <filesystem>
 #include <optional>
 #include <cassert>
+#include <thread>
 
 #include "DataTree.hh"
 #include "EquationTags.hh"
@@ -324,6 +325,9 @@ protected:
   /*! Maps endogenous type specific IDs to equation numbers */
   vector<int> endo2eq;
 
+  // Stores threads for compiling MEX files in parallel
+  static vector<jthread> mex_compilation_threads;
+
   /* Compute a pseudo-Jacobian whose all elements are either zero or one,
      depending on whether the variable symbolically appears in the equation */
   jacob_map_t computeSymbolicJacobian() const;
@@ -459,7 +463,12 @@ private:
   //! Finds a suitable GCC compiler on macOS
   static string findGccOnMacos(const string &mexext);
 #endif
-  //! Compiles a MEX file
+  /* Compiles a MEX file. The compilation is done in a separate asynchronous
+     thread, so the call to this function is not blocking.
+     TODO: further improve the function so that when a MEX has multiple source
+     files, those get compiled in separate threads; this could however
+     require implementing a scheduler, so as to not run more threads than
+     there are logical cores. */
   void compileMEX(const string &basename, const string &funcname, const string &mexext, const vector<filesystem::path> &src_files, const filesystem::path &matlabroot, const filesystem::path &dynareroot) const;
 
 public:
@@ -507,6 +516,9 @@ public:
      If no such equation can be found, throws an ExprNode::MatchFailureExpression */
   expr_t getRHSFromLHS(expr_t lhs) const;
 
+  // Calls join() on all MEX compilation threads
+  static void joinMEXCompilationThreads();
+
   //! Returns all the equation tags associated to an equation
   map<string, string>
   getEquationTags(int eq) const