diff --git a/doc/RTD/source/RadiativeTransfer/GEAR_RT.rst b/doc/RTD/source/RadiativeTransfer/GEAR_RT.rst
index 7b33e66df91e653a7769646af3112ed12cdb2f53..47fa46c6cca42d3ef61f3fed10734b90fa3de0de 100644
--- a/doc/RTD/source/RadiativeTransfer/GEAR_RT.rst
+++ b/doc/RTD/source/RadiativeTransfer/GEAR_RT.rst
@@ -22,7 +22,7 @@ Compiling for GEAR RT
 -   You need to choose a Riemann solver for the RT equations. You can choose
     between the ``GLF`` and ``HLL`` solver. For the time being, I recommend 
     sticking to the ``GLF`` solver as the ``HLL`` solver is more expensive,
-    but seemingly offers no advantage, although this remains to be comfirmed
+    but seemingly offers no advantage, although this remains to be confirmed
     in further testing.
 
 -   GEAR RT is only compatible with the Meshless Finite Volume scheme. You'll
@@ -59,6 +59,10 @@ You need to provide the following runtime parameters in the yaml file:
 
        stellar_spectrum_type: 0                           # Which radiation spectrum to use. 0: constant. 1: blackbody spectrum.
 
+   TimeIntegration:
+       max_nr_rt_subcycles: 128         # maximal number of RT subcycles per hydro step
+
+
 The ``photon_groups_Hz`` need to be ``N`` frequency edges (floats) to separate 
 the spectrum into ``N`` groups, where ``N`` is the same number you configured
 with using ``--with_rt=GEAR_N``. The edges are **lower** edges of the bins, and
@@ -111,6 +115,9 @@ to select between:
    e.g. equations 9 - 11 in `Rosdahl et al. 2013. 
    <https://ui.adsabs.harvard.edu/abs/2013MNRAS.436.2188R/abstract>`_
 
+Finally, you will also need to provide an upper threshold for the number of 
+RT-subcycles w.r.t. a single hydro step via ``TimeIntegration:max_nr_rt_subcycles``.
+For more details, refer to :ref:`the subcycling documentation <rt_subcycling>`.
 
 
 
@@ -393,7 +400,7 @@ useful:
    both methods can run on the exact same ICs.
 
 
-.. [#f2] For example, choosing cgs units as the interal units may lead to
+.. [#f2] For example, choosing cgs units as the internal units may lead to
    trouble with grackle. (Trouble like a gas at 10^6K without any heating
    sources heating up instead of cooling down.) The library is set up to work 
    with units geared towards cosmology. According to Britton Smith (private comm), 
diff --git a/doc/RTD/source/RadiativeTransfer/RTTaskDependencies.webp b/doc/RTD/source/RadiativeTransfer/RTTaskDependencies.webp
new file mode 100644
index 0000000000000000000000000000000000000000..b47231dcaf53bdbad0fe47b83a0f474e85dad396
Binary files /dev/null and b/doc/RTD/source/RadiativeTransfer/RTTaskDependencies.webp differ
diff --git a/doc/RTD/source/RadiativeTransfer/RTTaskDependenciesFull-simplified.png b/doc/RTD/source/RadiativeTransfer/RTTaskDependenciesFull-simplified.png
new file mode 100644
index 0000000000000000000000000000000000000000..349ab164179b4fe7c8ea240f881e9d821869647a
Binary files /dev/null and b/doc/RTD/source/RadiativeTransfer/RTTaskDependenciesFull-simplified.png differ
diff --git a/doc/RTD/source/RadiativeTransfer/RTWorkflow.png b/doc/RTD/source/RadiativeTransfer/RTWorkflow.png
new file mode 100644
index 0000000000000000000000000000000000000000..f3956de04d3313e46deff26901f0840255caa302
Binary files /dev/null and b/doc/RTD/source/RadiativeTransfer/RTWorkflow.png differ
diff --git a/doc/RTD/source/RadiativeTransfer/RT_notes_for_developers.rst b/doc/RTD/source/RadiativeTransfer/RT_notes_for_developers.rst
new file mode 100644
index 0000000000000000000000000000000000000000..286418d09d8976ba8900f312d0d4633114137a88
--- /dev/null
+++ b/doc/RTD/source/RadiativeTransfer/RT_notes_for_developers.rst
@@ -0,0 +1,490 @@
+.. RT developer notes
+    Mladen Ivkovic 07.2022
+
+.. _rt_dev:
+   
+Notes for Developers
+========================
+
+
+.. _rt_workflow:
+
+The Radiative Transfer Workflow
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This section is intended to give a superficial overview on the general workflow 
+that the RT tasks are going through. It should be sufficient to give you a general
+idea of how things work, and allow you to plan and implement your own RT scheme
+into SWIFT.
+
+For a more complete documentation on the tasking system used for RT, please refer
+to the :ref:`subsequent section <rt_task_system>`.
+
+.. figure:: RTWorkflow.png
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: Workflow for the Radiative Transfer in SWIFT.
+
+    This figure shows the general RT workflow in SWIFT.
+    The green nodes are the ones related to the RT.
+
+
+- There are some additions to star tasks: 
+
+  1)    During the star density loop, we gather additional neighbour information
+        that are required for the star-gas interaction.
+
+  2)    In the star ghost tasks, stars compute how much radiation they will 
+        inject in the surrounding gas. The radiation injection scheme follows 
+        the philosophy of the stellar feedback, and only stars that are 
+        currently active will inject radiation into neighbouring gas particles, 
+        regardless of whether the gas particle is currently active (as opposed 
+        to active gas particles "pulling" radiation from possibly inactive 
+        stars). So the each star needs to compute how much radiation it will 
+        emit during its current time step.
+
+  3)    During the stellar feedback interaction loop, the stars inject radiation
+        onto neighbouring gas particles.
+
+- ``Ghost1`` tasks operate on individual particles, and are intended to finish up
+  any leftover work from the injection.
+
+- ``Gradient`` tasks are particle-particle interaction tasks, intended for 
+  particles to gather data from its own neighbours, e.g. so we can estimate the 
+  current local gradients.  This is an interaction of "type 1", meaning that any 
+  particle will only interact with neighbours which are within its own compact 
+  support radius.
+
+- ``Ghost2`` tasks operate on individual particles, and are intended to finish 
+  up any leftover work from the "gradients".
+
+- ``Transport`` tasks are particle-particle interaction tasks, intended to 
+  propagate the radiation. This is an interaction of "type 2", meaning that any 
+  particle will interact with any neighbours within either particles' compact 
+  support radius.
+
+- ``thermochemistry`` tasks operate on individual particles, and are intended
+  to solve the thermochemistry equations.
+
+
+
+
+
+
+
+.. _rt_task_system:
+
+Current Task System
+~~~~~~~~~~~~~~~~~~~~
+
+Some RT tasks featured in the full task graphs below, like the 
+``rt_advance_cell_time``, ``rt_collect_times``, and ``rt_sorts``, have not been 
+mentioned in the previous section. They are necessary for internal machinations 
+of the RT subcycling scheme, and do not affect the RT scheme itself. If you are
+implementing a new RT scheme into SWIFT, you should not need to touch those
+tasks. For more documentation on them, please refer to the :ref:`subsequent
+section <rt_subcycling_documentation>`.
+
+
+.. figure:: RTTaskDependenciesFull-simplified.png
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: Task dependencies for the sink scheme.
+
+    This figure shows the task dependencies for the radiative transfer scheme.
+    Some tasks with little or no relevance to the RT scheme have been simplified
+    and condensed for clarity.
+    This was done with SWIFT v0.9.0.
+
+
+.. figure:: RTTaskDependencies.webp
+    :width: 400px
+    :align: center
+    :figclass: align-center
+    :alt: Task dependencies for the sink scheme.
+
+    This figure shows the full task dependencies for the radiative transfer scheme
+    with self-gravity.
+    This was done with SWIFT v0.9.0.
+
+
+
+
+
+
+.. _rt_subcycling_documentation:
+
+Notes on Subcycling
+~~~~~~~~~~~~~~~~~~~~~
+
+Note: This section is directed towards developers and maintainers, not
+necessarily towards users.
+
+How it works
+`````````````````
+
+A subcycle is basically a SWIFT time step where only radiative transfer is being
+run.
+
+After a normal SWIFT time step (i.e. after a call to ``engine_launch()`` and the
+global collection and communication) is complete, the starting time of the
+following global time step is known. We also collect the current minimal RT time 
+step size, which allows us to determine how many sub-cycles we need to complete
+before the next normal SWIFT time step is launched. Particles are not drifted 
+during a subcycle, and the propagation velocity (aka the speed of light) is 
+taken to be constant, so the number of subcycles is fixed at the end of a normal 
+step. For each subcycle, we then unskip the RT tasks, and make a new call to
+``engine_launch()``.
+
+For the time integration to work correctly, the time integration variables of
+particles like the time-bins are kept independently from the hydro ones. The same
+goes for the respective quantities of cells, like the next integer end time of
+the cell, or the minimal RT time step size in the cell. Furthermore, the global
+time variables that are stored in the engine (e.g. current integer time, current
+max active bin...) have a copy that is being kept up-to-date outside of normal
+SWIFT steps in the same manner the non-RT variables are being updated each
+normal step. The RT subcycling scheme never touches or changes any global time
+integration related variable.
+
+Since the time stepping variables of particles and cells are taken to be
+constant during subcycles (because there are no drifts, constant speed of
+light), the ``timestep`` tasks are not being run during a sub-cycle. This
+effectively means that the particle time bins can only be changed in a normal
+step when the particle is also hydro-active. Furthermore, there are no MPI 
+communications after the tasks have finished executing to update any global 
+times etc. for the same reason. There are some functionalities of the
+``timestep`` and the ``collect`` tasks which are still necessary though:
+
+- The ``timestep`` task also updates the cell's next integer end time after it
+  has been determined during the task. During a subcycle, the next end time is
+  simply the current time plus the minimal time step size of the cell, but we
+  need a task to actually update the cell at the end of each subcycle. The
+  ``rt_advance_cell_time`` task does exactly that, and in that sense does the
+  ``timestep`` task's job during subcycles.
+
+- The ``collect`` task propagates sub-cell data like the minimal end time or the
+  RT time step size from the super level to the top level. This functionality is
+  replaced with the ``rt_collect_times`` tasks during subcycles. Note that the
+  ``rt_collect_times`` tasks aren't being activated during normal steps, as the
+  ``collect`` tasks already do the job just fine.
+
+Something special about the ``rt_advance_cell_time`` tasks is that they are
+also created and run on foreign cells. During a subcycle, the ``tend`` tasks
+don't run and don't update the cell time variables from the original cell, so
+during the subsequent unskipping, the data will be wrong, leading to all sorts
+of trouble. We can do that on foreign cells during sub-cycles because all the
+cell's time step sizes stay fixed between two regular SWIFT steps, and hence
+the number of sub-cycles all the sub-cycles' end times are predictable.
+
+
+
+
+RT Sorts
+````````````````
+
+The sorting of particles required for pair-type interaction tasks requires some
+special attention. The issues arise because a subcycle step of a cell can
+coincide with the main step of another cell. To illustrate, suppose we have two
+cells, ``A`` and ``B``. Let cell ``A`` have a hydro time step of size 4, and 
+cell ``B`` a hydro time step of size 8. Let both cells do 2 RT subcycles per
+hydro step each. In the graph below, an ``X`` represents when a cell will be
+updated:
+
+.. code::
+
+   Cell A
+     Hydro active:    X               X               X               X               X       
+     RT active:       X       X       X       X       X       X       X       X       X       X
+
+   Cell B
+     Hydro active:    X                               X                               X
+     RT active:       X               X               X               X               X       
+    
+    
+    ------------------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
+    t                 0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18
+
+
+Note that e.g. at ``t`` = 4, cell ``B`` is only RT active, while cell ``A`` is
+also hydro active. It being hydro active means that we will have a SWIFT main
+step at that time.
+
+Now suppose cell cells ``A`` and ``B`` are neighbouring cells that undergo hydro
+interactions, but are on different MPI ranks. For the hydro interactions in the
+normal SWIFT step at ``t`` = 4, cell ``B`` will be sent over to the rank of 
+cell ``A``. Once it was received, it will be sorted, because after being 
+received, the ``recv_xv`` task resets the arrived cell's sort tasks, and a hydro 
+sort is activated. This is the default hydrodynamics workflow.
+
+
+
+Complications however arise when several conditions coincide:
+
+- a foreign cell has been drifted on its "home" domain due to some reason other 
+  than hydro, e.g. for gravity or for stellar feedback. 
+- the foreign cell and all of its neighbours are not hydro active at the current
+  main step, so no ``recv_xv`` task has been run on the local rank, no ``sort``
+  task has been run, and the sorting flags have not been reset for the cell.
+- the foreign cell is involved in an RT interaction that coincides with the 
+  current main step, i.e. the cell or one of its neighbours is RT active during
+  a main step. (E.g. like cell ``A`` at ``t`` = 2 in the graph above.)
+
+If these conditions are met, the cell will undergo an interaction while
+unsorted. Obviously that's a no-no.
+
+
+To illustrate this problem, consider the following scenario. Say we have cells ``A``,
+``B``, and ``C``. Cell ``A`` is "home" on MPI rank 0, while cell ``B`` is on MPI rank 1. 
+In the current step in this scenario, cell ``A`` and ``B`` are both active for RT, and 
+interact with each other. ``C`` has an active star, which inject energy into particles of
+``B``.  Cell ``A`` and ``C`` do *not* interact with each other:
+
+
+.. code::
+
+   rank 0       | rank 1  
+                |
+      RT interaction        Star Feedback  
+   A <----------|---> B <------------------ C
+                |
+                |
+
+
+Since ``A`` and ``B`` interact, but are separated between different MPI ranks, both ``A``
+and ``B`` will have local copies of each other available on their local ranks, respectively.
+These cells are referred to as "foreign cells". Let's denote them with an additional ``f``:
+
+.. code::
+
+    rank 0                      | rank 1  
+                                |
+             RT interaction     |        RT interaction      Star Feedback  
+           A <-------------> Bf |   Af  <-------------> B <------------------ C
+                             ^  |   ^
+    (this is a foreign cell) |  |   | (this is a foreign cell)
+
+
+Now let's first have a look at the default workflow when hydrodynamics is involved. 
+Assume that all cells ``A``, ``B``, and ``C`` are hydro *and* RT active for this step.
+Once again cells ``A`` and ``B`` interact, and ``B`` and ``C`` interact, but ``A`` 
+and ``C`` *don't* interact. Cell ``C`` contains an active star particle.
+
+**Without** MPI, the order of operations for each cell would look like this: (operations 
+where two cells interact with each other are marked with an arrow)
+
+
+.. code::
+
+    A                   B                  C
+    ----------------------------------------------------
+    Drift               Drift               Drift
+    Sort                Sort                Sort
+    Density Loop <----> Density Loop <----> Density Loop
+    Ghost               Ghost               Ghost
+    Force Loop <------> Force Loop <------> Force Loop
+    End Force           End Force           End Force
+    Kick2               Kick2               Kick2
+
+                                            Star Drift
+                                            Star Sort
+                        (inactive) <------> Star Density
+                                            Star Ghost
+                        (inactive) <------> Star Feedback
+
+    RT Ghost1           RT Ghost1           RT Ghost1
+    RT Gradient <-----> RT Gradient <-----> RT Gradient
+    RT Ghost2           RT Ghost2           RT Ghost2
+    RT Transport <----> RT Transport <----> RT Transport
+    RT Tchem            RT Tchem            RT Tchem
+    Timestep            Timestep            Timestep
+    Kick1               Kick1               Kick1
+
+
+Now  **with** MPI communications, cells ``A`` and ``B`` need to send over the 
+up-to-date data to their foreign counterparts ``Af`` and ``Bf``, respectively, 
+*before* each interaction type task (the ones with arrows in the sketch above). 
+The order of operations should look like this:
+(The foreign cell ``Af`` on rank 1 is omitted for clarity, but follows the same
+principle as ``Bf`` on rank 0)
+
+.. code::
+
+    rank 0                                  |  rank 1
+    A                   Bf                  |  B                   C
+    ----------------------------------------|----------------------------------------
+    Drift                                   |  Drift               Drift
+                        Recv XV  <------------ Send XV
+    Sort                Sort                |  Sort                Sort
+    Density Loop <----> Density Loop        |  Density Loop <----> Density Loop
+    Ghost                                   |  Ghost               Ghost
+                        Recv Density <-------- Send Density
+    Force Loop <------> Force Loop          |  Force Loop <------> Force Loop
+    End Force                               |  End Force           End Force
+    Kick2                                   |  Kick2               Kick2
+                                            |
+                                            |                      Star Drift
+                                            |                      Star Sort
+                                            |  (inactive) <------> Star Density
+                                            |                      Star Ghost
+                                            |  (inactive) <------> Star Feedback
+                                            |
+    RT Ghost1                               |  RT Ghost1           RT Ghost1
+                        Recv RT Gradient <---- Send RT Gradient
+    RT Gradient <-----> RT Gradient         |  RT Gradient <-----> RT Gradient
+    RT Ghost2                               |  RT Ghost2           RT Ghost2
+                        Recv RT Transport <--- Send RT Transport
+    RT Transport <----> RT Transport        |  RT Transport <----> RT Transport
+    RT Tchem                                |  RT Tchem            RT Tchem
+    Timestep                                |  Timestep            Timestep
+                        Recv tend <----------- Send tend
+    Kick1                                   |  Kick1               Kick1
+
+
+Finally, let's look at the scenario which causes problems with the sort. This 
+is the case, as described above, when (a) cells ``A`` and ``B`` are RT active during
+a main step (like in the sketch above), (b) aren't hydro active during a main step 
+(unlike what is drawn above), (c) one of these cells is foreign (in this case, ``Bf``),
+while the "home" cell (cell ``B``) get drifted during a main step for some reason
+other than hydrodynamics, e.g. because a star interaction with cell ``C`` requested it.
+
+In this case, the workflow looks like this:
+
+.. code::
+
+    rank 0                                  |  rank 1
+    A                   Bf                  |  B                   C
+    ----------------------------------------|----------------------------------------
+                                            |  Drift               Drift
+                                            |  Sort                Sort
+                                            |                      Kick2
+                                            |
+                                            |                      Star Drift
+                                            |                      Star Sort
+                                            |  (inactive) <------> Star Density
+                                            |                      Star Ghost
+                                            |  (inactive) <------> Star Feedback
+                                            |
+    RT Ghost1                               |  RT Ghost1           RT Ghost1
+                        Recv RT Gradient <---- Send RT Gradient
+    RT Gradient <-----> RT Gradient         |  RT Gradient <-----> RT Gradient
+    RT Ghost2                               |  RT Ghost2           RT Ghost2
+                        Recv RT Transport <--- Send RT Transport
+    RT Transport <----> RT Transport        |  RT Transport <----> RT Transport
+    RT Tchem                                |  RT Tchem            RT Tchem
+    Timestep                                |  Timestep            Timestep
+                        Recv tend <----------- Send tend
+    Kick1                                   |  Kick1               Kick1
+
+The issue is that with the missing hydro communication tasks, the first communication
+between cell ``B`` and its foreign counterpart ``Bf`` is the ``recv_rt_gradient`` task.
+Recall that during a communication, we always send over all particle data of a cell.
+This includes all the particle positions, which may have been updated during a drift.
+However, the sorting information is not stored in particles, but in the cell itself.
+For this reason, a ``sort`` task is *always* run directly after a cell finishes the 
+``recv_xv`` task, which, until the sub-cycling was added, was always the first task any 
+foreign cell would run, with a subsequent ``sort``.
+
+The RT sub-cycling now however allows the ``recv_xv`` task to not run at all
+during a main step, since it's not always necessary, as shown in the example above.
+All the required data for the RT interactions can be sent over with
+``send/recv_rt_gradient`` tasks. An unintended consequence however is that in a
+scenario as sketched above, at the time of the ``A <---> Bf`` RT Gradient
+interaction, cell ``Bf`` will not be sorted. That's a problem.
+
+To solve this issue, a new task has been added, named ``rt_sorts``. It is only
+required for foreign cells, like cell ``Bf``, and only during normal/main steps 
+(as we don't drift during subcycles, there won't be any reason to re-sort.) On
+local cells, each time a drift is activated for an interaction type task, the 
+sort task is also activated. So there is no need for ``rt_sorts`` tasks on local
+cells.
+An additional advantage to adding a new sort task like the ``rt_sorts`` is that
+it allows us to sidestep possible deadlocks. Suppose that as an alternative to
+the ``rt_sort`` tasks we instead use the regular hydro ``sort`` task. The default
+hydro ``sort`` task is set up to run before the other hydro tasks, and in 
+particular before the ``kick2`` task. However the RT and star tasks are executed 
+*after* the ``kick2``. This means that there are scenarios where a cell with a 
+foreign counterpart like cells ``B`` and ``Bf`` can deadlock when ``Bf`` is waiting
+for the ``recv_rt_gradient`` to arrive so it may sort the data, while ``B`` is
+waiting for ``Bf`` to finish the sorting and proceed past the ``kick2`` stage so
+it can run the ``send_rt_gradient`` data which would allow ``Bf`` to run the
+sorts.
+
+
+The ``rt_sorts`` tasks are executed after the first RT related ``recv``, in this 
+case the ``recv_rt_gradient``. The order of operations should now look like this:
+
+
+.. code::
+
+    rank 0                                  |  rank 1
+    A                   Bf                  |  B                   C
+    ----------------------------------------|----------------------------------------
+                                            |  Drift               Drift
+                                            |  Sort                Sort
+                                            |                      Kick2
+                                            |
+                                            |                      Star Drift
+                                            |                      Star Sort
+                                            |  (inactive) <------> Star Density
+                                            |                      Star Ghost
+                                            |  (inactive) <------> Star Feedback
+                                            |
+    RT Ghost1                               |  RT Ghost1           RT Ghost1
+                        Recv RT Gradient <---- Send RT Gradient
+                        rt_sort             |
+    RT Gradient <-----> RT Gradient         |  RT Gradient <-----> RT Gradient
+    RT Ghost2                               |  RT Ghost2           RT Ghost2
+                        Recv RT Transport <--- Send RT Transport
+    RT Transport <----> RT Transport        |  RT Transport <----> RT Transport
+    RT Tchem                                |  RT Tchem            RT Tchem
+    Timestep                                |  Timestep            Timestep
+                        Recv tend <----------- Send tend
+    Kick1                                   |  Kick1               Kick1
+
+
+
+In order to minimize unnecessary work, three new cell flags concerning the RT
+sorts have been added:
+ 
+- ``cell_flag_do_rt_sub_sort``: tracks whether we need an RT sub sort, which is 
+  equivalent to the ``cell_flag_do_sub_sort`` flag for hydro. We can't use the 
+  hydro flag though because the hydro flag is also used to early-exit walking up 
+  the cell hierarchy when activating hydro subcell sorts. In particular, this
+  condition in ``cell_unskip.c:cell_activate_hydro_sorts_up():``
+
+.. code:: 
+
+   void cell_activate_hydro_sorts_up(struct cell *c, struct scheduler *s) {
+       /* omitted lines */
+
+       for (struct cell *parent = c->parent;
+           parent != null && !cell_get_flag(parent, cell_flag_do_hydro_sub_sort); parent = parent->parent) {
+       /* !! this is the problem ---^ */
+   
+       /* omitted lines */
+     }
+   }
+
+The sort activation for RT and for hydro can run concurrently. So there is no
+guarantee that when the hydro sort activation sets the
+``cell_flag_do_hydro_sub_sort`` flag, the RT sorting tasks will be activated
+correctly, which occurs at the top of the cell hierarchy tree walk.
+So we need an independent flag for RT here to not abort the tree walk early
+and in error.
+
+- ``cell_flag_do_rt_sort``: tracks whether the call to the 
+  ``runner_do_hydro_sort()`` function was requested by an RT sort. (Both the (hydro) 
+  ``sort`` and the ``rt_sort`` tasks call the same function.) This flag is used to 
+  discriminate during the actual sorting in the ``runner_do_hydro_sort()``
+  function if the internal check whether the cell is drifted to the current time
+  may be disregarded. When an RT subcycle coincides with a main step, the particles 
+  won't necessarily be drifted to the current time as there is no need to drift them 
+  for RT only. This is intended behaviour, so we allow ``runner_do_hydro_sort()`` to
+  skip this drift check in the case where the sorting was requested for RT purposes.
+
+- ``cell_flag_skip_rt_sort``: Tracks whether a regular hydro sort has been
+  activated for this cell. If it has, then there is no need to run an RT sort as
+  well, and we skip it.
+
diff --git a/doc/RTD/source/RadiativeTransfer/RT_subcycling.rst b/doc/RTD/source/RadiativeTransfer/RT_subcycling.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d5e8a5151ad2c9d09f996cc8fe19bc1315dd274c
--- /dev/null
+++ b/doc/RTD/source/RadiativeTransfer/RT_subcycling.rst
@@ -0,0 +1,54 @@
+.. RT Subcycling
+    Mladen Ivkovic 07.2022
+
+.. _rt_subcycling:
+   
+RT Subcycling
+-------------
+
+.. warning::
+    The radiative transfer schemes are still in development and are not useable
+    at this moment. This page is currently a placeholder to document new
+    features and requirements as the code grows.
+
+
+SWIFT allows to sub-cycle the solution of radiative transfer steps (both 
+photon propagation and thermochemistry) with respect to the hydrodynamics
+time steps. Basically you can tell SWIFT to run up to X radiative transfer
+steps during a single hydrodynamics step for all particles in the simulation.
+The aim is to not waste time doing unnecessary hydrodynamics updates, which
+typically allow for much higher time steps compared to radiation due to the
+propagation speed of the respective advected quantity.
+
+You will need to provide an upper limit on how many RT subcycles per hydro
+step you want to allow. That is governed by the
+
+.. code:: yaml
+
+   TimeIntegration:
+       max_nr_rt_subcycles: 128         # maximal number of RT subcycles per hydro step
+
+parameter, which is mandatory for any RT runs. To turn off subcycling and 
+couple the radiative transfer and the hydrodynamics time steps one-to-one,
+set this parameter to either 0 or 1.
+
+Due to the discretization of individual particle time steps in time bins
+with a factor of 2 difference in time step size from a lower to a higher
+time bin, the ``max_nr_rt_subcycles`` parameter itself is required to be
+a power of 2 as well.
+
+Note that this parameter will set an upper limit to the number of subcycles
+per hydro step. If the ratio of hydro-to-RT time step is greater than what
+``max_nr_rt_subcycles`` allows for, then the hydro time step will be reduced
+to fit the maximal threshold. If it is smaller, the particle will simply do 
+fewer subcycles.
+
+.. warning::
+   Contrary to the documentation above, in the current implementation the 
+   ``max_nr_rt_subcycles`` parameters is abused as the fixed number of RT 
+   subcycles per hydro step. This means that RT time steps will be reduced
+   to lower values than necessary to fit the exact ratio should they be too
+   large to begin with.
+   Once the development advances, the behaviour will be set to be as 
+   documented above.
+
diff --git a/doc/RTD/source/RadiativeTransfer/index.rst b/doc/RTD/source/RadiativeTransfer/index.rst
index 0eafdfeca2483f91ffe49bd0b99ba0403d00e823..2f465b735f34b9bb63c5bde0a006fba4eb1441e8 100644
--- a/doc/RTD/source/RadiativeTransfer/index.rst
+++ b/doc/RTD/source/RadiativeTransfer/index.rst
@@ -24,4 +24,6 @@ schemes.
    requirements
    GEAR_RT
    SPHM1_RT
+   RT_subcycling
+   RT_notes_for_developers
    additional_tools
diff --git a/doc/RTD/source/Task/current_dependencies.rst b/doc/RTD/source/Task/current_dependencies.rst
index 63b1cb62f7a2776e255da1f1c2ce408e09319133..47ff74c6afaab23a87079c9824b53c6a8d5f5479 100644
--- a/doc/RTD/source/Task/current_dependencies.rst
+++ b/doc/RTD/source/Task/current_dependencies.rst
@@ -68,3 +68,6 @@ As the hydrodynamics are described in :ref:`hydro`, we are only showing the grav
     In the second one, the gas particles tagged as "to be swallowed" are effectively swallowed.
     In the third one, the sink particles tagged as "to be swallowed" are effectively swallowed.
     This was done with SWIFT v0.9.0.
+
+For documentation on the radiative transfer tasking system, please refer to its 
+:ref:`own page <rt_task_system>`.
diff --git a/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml b/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml
index f85a1d526b140b91a0a46c1d77b97f81952c862b..24641c29fcd54b428e600807b7139a4ea525dea2 100644
--- a/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml
+++ b/examples/RadiativeTransferTests/Advection_1D/rt_advection1D.yml
@@ -11,6 +11,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   4.e-1  # The end time of the simulation (in internal units).
   dt_min:     1.e-8 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/Advection_1D/run.sh b/examples/RadiativeTransferTests/Advection_1D/run.sh
index f0a3f3faa9e0283359adbbe1aede36edfa01853d..0b64a9b773f87b40c1237b91960bd09311da6c38 100755
--- a/examples/RadiativeTransferTests/Advection_1D/run.sh
+++ b/examples/RadiativeTransferTests/Advection_1D/run.sh
@@ -18,7 +18,7 @@ fi
     --stars \
     --feedback \
     --external-gravity \
-    -e \
+    --fpe \
     ./rt_advection1D.yml 2>&1 | tee output.log
 
 python3 ./plotSolution.py
diff --git a/examples/RadiativeTransferTests/Advection_2D/rt_advection2D.yml b/examples/RadiativeTransferTests/Advection_2D/rt_advection2D.yml
index b79dfe142670c4c63952e98bd61c5cb6b1e523ab..e010f53e94761ff544fbe364cf51887b214b8a09 100644
--- a/examples/RadiativeTransferTests/Advection_2D/rt_advection2D.yml
+++ b/examples/RadiativeTransferTests/Advection_2D/rt_advection2D.yml
@@ -11,6 +11,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.     # The starting time of the simulation (in internal units).
   time_end:   3.4e-1 # The end time of the simulation (in internal units).
   dt_min:     1.e-08 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/Advection_2D/run.sh b/examples/RadiativeTransferTests/Advection_2D/run.sh
index 2ea05d8e385609e23036943121b2b2a1f13838f1..56851dab7bec3104c985b77dde565557dc6ec995 100755
--- a/examples/RadiativeTransferTests/Advection_2D/run.sh
+++ b/examples/RadiativeTransferTests/Advection_2D/run.sh
@@ -25,7 +25,7 @@ fi
     --stars \
     --feedback \
     --external-gravity \
-    -e \
+    --fpe \
     ./rt_advection2D.yml 2>&1 | tee output.log
 
 python3 ./plotSolution.py
diff --git a/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml b/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml
index ee95238d63db4841f580ffdb31eec7fe4c4f13c3..614760dff46959b19509e48c9a13a4ea131061b2 100644
--- a/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml
+++ b/examples/RadiativeTransferTests/CoolingTest/rt_cooling_test.yml
@@ -12,6 +12,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.     # The starting time of the simulation (in internal units).
   time_end:   0.100  # The end time of the simulation (in internal units).
   dt_min:     1.e-8  # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/HeatingTest/rt_heating_test.yml b/examples/RadiativeTransferTests/HeatingTest/rt_heating_test.yml
index 904e4508a8e74877221f303e175ef4e9aa35ccf9..a68955534df580eb432b37d092a9b4ae44086453 100644
--- a/examples/RadiativeTransferTests/HeatingTest/rt_heating_test.yml
+++ b/examples/RadiativeTransferTests/HeatingTest/rt_heating_test.yml
@@ -12,6 +12,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.     # The starting time of the simulation (in internal units).
   dt_min:     1.0208453377e-08  # 0.01 yr
   dt_max:     0.0010208453      # 1 kyr
diff --git a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml
index b5bde60996863598b45a74cc7c75448555f85eb7..bca226b52d2e3adb2ee7537b81a6c733c0ce63c6 100644
--- a/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml
+++ b/examples/RadiativeTransferTests/IonMassFractionAdvectionTest_2D/advect_ions.yml
@@ -11,6 +11,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.01   # end time: radiation reaches edge of box
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/IonizationEquilibriumICSetupTest/ion_equil.yml b/examples/RadiativeTransferTests/IonizationEquilibriumICSetupTest/ion_equil.yml
index b3590ed6eb25e3239929f7fb8768c412d3d3fa65..426491c281a8ed9ad9af22c0dfd435914b3b8be0 100644
--- a/examples/RadiativeTransferTests/IonizationEquilibriumICSetupTest/ion_equil.yml
+++ b/examples/RadiativeTransferTests/IonizationEquilibriumICSetupTest/ion_equil.yml
@@ -11,6 +11,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   6.250000e-03  # The end time of the simulation (in internal units).
   dt_min:     1.e-6 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/RandomizedBox_3D/README b/examples/RadiativeTransferTests/RandomizedBox_3D/README
index 03d686ed719ea8693409699cae4f505362f04a73..1d8fb2855841811b5e7d05ad521e4f5af5e0a485 100644
--- a/examples/RadiativeTransferTests/RandomizedBox_3D/README
+++ b/examples/RadiativeTransferTests/RandomizedBox_3D/README
@@ -15,3 +15,12 @@ check that everything's right.
 
 Compile swift with:
     --with-rt=GEAR_1 --with-rt-riemann-solver=GLF --with-hydro-dimension=3 --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --enable-debugging-checks --with-grackle=$GRACKLE_ROOT
+
+
+
+Some specific notes for debugging:
+
+-   when running on 2 MPI ranks (at least in the first few steps) cells 27 and
+    151 have proxies on the other rank. However, those are made for the gravity
+    tasks, not for the RT tasks. So their foreign counterparts will have no RT
+    tasks at all.
diff --git a/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml b/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml
index d0c4cd913e9aae69372dcaaa842988f90ead0476..2c6b5dab640d8642d069ca51ebc7f55f7eb823db 100644
--- a/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml
+++ b/examples/RadiativeTransferTests/RandomizedBox_3D/randomized-rt.yml
@@ -11,9 +11,10 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 4
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.1   # The end time of the simulation (in internal units).
-  dt_min:     1.e-8 # The minimal time-step size of the simulation (in internal units).
+  dt_min:     1.e-6 # The minimal time-step size of the simulation (in internal units).
   dt_max:     1.e-3 # The maximal time-step size of the simulation (in internal units).
 
 # Parameters governing the snapshots
@@ -49,7 +50,7 @@ Scheduler:
 Restarts:
   delta_hours:        72        # (Optional) decimal hours between dumps of restart files.
   enable:             1          # (Optional) whether to enable dumping restarts at fixed intervals.
-  stop_steps:         500        # (Optional) how many steps to process before checking if the <subdir>/stop file exists. When present the application will attempt to exit early, dumping restart files first.
+  stop_steps:         128        # (Optional) how many steps to process before checking if the <subdir>/stop file exists. When present the application will attempt to exit early, dumping restart files first.
 
 # Parameters for the self-gravity scheme
 Gravity:
diff --git a/examples/RadiativeTransferTests/RandomizedBox_3D/run.sh b/examples/RadiativeTransferTests/RandomizedBox_3D/run.sh
index 174fda2baad152abcd52c3abcd2afc1f34801e88..e2171960cfc21169ffb767ccf265cc9071697806 100755
--- a/examples/RadiativeTransferTests/RandomizedBox_3D/run.sh
+++ b/examples/RadiativeTransferTests/RandomizedBox_3D/run.sh
@@ -9,12 +9,26 @@ if [ ! -f 'randomized-sine.hdf5' ]; then
     python3 makeIC.py
 fi
 
+# use cmdline args as shortcut to run with debugger/MPI
+# -g run with gdb
+# -m run with MPI
+# -mg run with MPI and gdb in individual xterm windows
+# -ml run with MPI and write individual output file per MPI rank
 cmd=../../../swift
 if [ $# -gt 0 ]; then
     case "$1" in 
-    g | gdb)
+    -g | g | gdb)
         cmd='gdb --args ../../../swift'
         ;;
+    -m | m | mpi)
+        cmd='mpirun -n 3 ../../../swift_mpi' 
+        ;;
+    -mg | -gm | gm | mg | gmpi | gdbmpi )
+        cmd='mpirun -n 3 xterm -e gdb -ex run --args ../../../swift_mpi'
+        ;;
+    -ml | ml | lm | mpilog | logmpi)
+        cmd='mpirun -n 2 --output-filename individual_rank_output --merge-stderr-to-stdout ../../../swift_mpi'
+        ;;
     *)
         echo unknown cmdline param, running without gdb
         ;;
@@ -24,8 +38,8 @@ fi
 # Run SWIFT with RT
 $cmd \
     --hydro \
-    --threads=9 \
-    --verbose=0  \
+    --threads=3 \
+    --verbose=0 \
     --radiation \
     --self-gravity \
     --stars \
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_2D/plotSolution.py b/examples/RadiativeTransferTests/StromgrenSphere_2D/plotSolution.py
index 231781bf3647dbc2767c141a08b94dad2535281e..db30f4ebd725bce1e848daed7c4f74c529e4ffd1 100755
--- a/examples/RadiativeTransferTests/StromgrenSphere_2D/plotSolution.py
+++ b/examples/RadiativeTransferTests/StromgrenSphere_2D/plotSolution.py
@@ -308,3 +308,4 @@ if __name__ == "__main__":
 
     for f in snaplist:
         plot_result(f)
+        gc.collect()
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_2D/propagationTest-2D.yml b/examples/RadiativeTransferTests/StromgrenSphere_2D/propagationTest-2D.yml
index 0860e86aaf8776e5db8e3155f2c860b45d914dca..2bbcd3e13a262d4036e892a2b1eec3c3bac35706 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_2D/propagationTest-2D.yml
+++ b/examples/RadiativeTransferTests/StromgrenSphere_2D/propagationTest-2D.yml
@@ -11,6 +11,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.001 # end time: radiation reaches edge of box
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
@@ -36,7 +37,7 @@ SPH:
 # Parameters related to the initial conditions
 InitialConditions:
   file_name:  ./propagationTest-2D.hdf5     # The file to read
-  periodic:   1                             # peridioc ICs. Keep them periodic so we don't loose photon energy. TODO: CHANGE LATER WHEN YOU ACTUALLY DO GAS INTERACTIONS
+  periodic:   1                             # peridioc ICs. Keep them periodic so we don't loose photon energy.
 
 GEARRT:
   f_reduce_c: 1.        # reduce the speed of light for the RT solver by multiplying c with this factor
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_2D/stromgrenSphere-2D.yml b/examples/RadiativeTransferTests/StromgrenSphere_2D/stromgrenSphere-2D.yml
index d9f77d2398e1627131e010c3c9fd0d96670ed4c5..848d5acce77297e41630017d6973cd741f52f85e 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_2D/stromgrenSphere-2D.yml
+++ b/examples/RadiativeTransferTests/StromgrenSphere_2D/stromgrenSphere-2D.yml
@@ -11,6 +11,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 8
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.512 
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_3D/propagationTest-3D.yml b/examples/RadiativeTransferTests/StromgrenSphere_3D/propagationTest-3D.yml
index ad07cddb8c9b887b1f58c5c3279876c828c2acbe..5e72319c40a840048dd4b7896d1c5dbf13dc9a02 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_3D/propagationTest-3D.yml
+++ b/examples/RadiativeTransferTests/StromgrenSphere_3D/propagationTest-3D.yml
@@ -11,6 +11,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 8
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.001 # end time: radiation reaches edge of box
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MF.yml b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MF.yml
index 0ce97607bfb562b3920ca5c01f1472d992f30c55..5d6c66014e3a44fc8caec6c256d6536758e2d06a 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MF.yml
+++ b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MF.yml
@@ -12,6 +12,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.112 # end time
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MFHHe.yml b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MFHHe.yml
index eba7bad6213173caefedf5cc170cd67cb908e1c9..0ca4391e590b934947689fe1ae9713268acb3ae9 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MFHHe.yml
+++ b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-MFHHe.yml
@@ -12,6 +12,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.112 # end time
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-singlebin.yml b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-singlebin.yml
index a289d85ad54dd6f16c3513618859e98e652a9959..49f1deb8600cd11d1bc2d3bc79ed8f6b130d1ff8 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-singlebin.yml
+++ b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D-singlebin.yml
@@ -12,6 +12,7 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
+  max_nr_rt_subcycles: 1
   time_begin: 0.    # The starting time of the simulation (in internal units).
   time_end:   0.512 # end time
   dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
diff --git a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D.yml b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D.yml
index de5b56ffa3b0c94757aae402ed4e97187ed2f322..91ce08c83ef0b0b742640caa89b8679372419044 100644
--- a/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D.yml
+++ b/examples/RadiativeTransferTests/StromgrenSphere_3D/stromgrenSphere-3D.yml
@@ -12,9 +12,10 @@ InternalUnitSystem:
 
 # Parameters governing the time integration
 TimeIntegration:
-  time_begin: 0.    # The starting time of the simulation (in internal units).
-  time_end:   0.512 # end time: radiation reaches edge of box
-  dt_min:     1.e-12 # The minimal time-step size of the simulation (in internal units).
+  max_nr_rt_subcycles: 1
+  time_begin: 0.      # The starting time of the simulation (in internal units).
+  time_end:   0.512   # end time
+  dt_min:     1.e-12  # The minimal time-step size of the simulation (in internal units).
   dt_max:     1.e-03  # The maximal time-step size of the simulation (in internal units).
 
 # Parameters governing the snapshots
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks-GEAR.py b/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks-GEAR.py
index 8b676b63f9f27df2c9f9c63ffb029f5ec6b704bc..63f9491356179e1b498841d3b6e68d85e5bf9a46 100755
--- a/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks-GEAR.py
+++ b/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks-GEAR.py
@@ -55,8 +55,8 @@ skip_plots = False  # skip showing plots for diagnosis
 float_comparison_tolerance = 1e-4
 # tolerance for a float that was summed up over all particles to vary
 float_particle_sum_comparison_tolerance = 5e-4
-# tolerance for meshless energy distribution scheme during injeciton comparison
-float_psi_comparison_tolerance = 5e-4
+# tolerance for energy conservation and injection during injeciton comparison
+energy_conservation_tolerance = 1e-3
 
 # -------------------------------------------------------------------------------
 
@@ -237,14 +237,18 @@ def check_injection(snapdata, rundata):
     #  Remember: The reason we have too little injected energy is because we
     #  don't inject any energy during the zeroth time step. We can't, since the
     #  zeroth time step is the one that determines the time step size of the star.
+    #  Also the star-feedback loop is skipped.
 
     # TODO: this assumes a constant number of stars. You need to deal with SF
     # TODO: this assumes no cosmological expansion
 
+    # upper boundaries (analytically expected values) for the plots
     upper_boundary_for_plot = []
+    # snapshot numbers, used as x-axis in plots
     snaps_for_1bplot = []
 
     initial_time = snapdata[0].time
+
     if snapdata[0].has_stars:
         emission_at_initial_time = snapdata[0].stars.InjectedPhotonEnergy.sum(axis=0)
     else:
@@ -252,81 +256,89 @@ def check_injection(snapdata, rundata):
             np.zeros(rundata.ngroups, dtype=np.float64) * unyt.erg
         )
 
-    if rundata.use_const_emission_rate:
+    continue_test = True
+
+    if not rundata.use_const_emission_rate:
+        print("Can't run check 1b without constant emission rates")
+        continue_test = False
+
+    if continue_test:
         if len(snapdata) <= 2:
             # because it's useless to check only snap_0000
             print("Check 1b: You need at least 2 snapshots to do this particular test")
-        else:
-            diffs_for_plot = []
-            energies_for_plot = []
-            found_potential_error = False
-            for snap in snapdata[1:]:  # skip snapshot zero
-                dt = snap.time - initial_time
-                if snap.has_stars:
-                    injected_energies = np.atleast_1d(
-                        snap.stars.InjectedPhotonEnergy.sum(axis=0)
-                        - emission_at_initial_time
+            continue_test = False
+
+    if continue_test:
+
+        diffs_for_plot = []  # relative difference between expectation and data
+        energies_for_plot = []  # the injected energies that were found in data
+        found_potential_error = False
+
+        for snap in snapdata[1:]:  # skip snapshot zero
+            dt = snap.time - initial_time
+            if snap.has_stars:
+                injected_energies = np.atleast_1d(
+                    snap.stars.InjectedPhotonEnergy.sum(axis=0)
+                    - emission_at_initial_time
+                )
+            else:
+                injected_energies = np.zeros(ngroups) * unyt.erg
+            # get what energies we expect the stars to have injected
+            energies_expected = snap.nstars * emission_rates * dt
+            energies_expected = energies_expected.to(injected_energies.units)
+            # get relative difference
+            diff = np.array(injected_energies / energies_expected - 1.0)
+
+            # store data
+            upper_boundary_for_plot.append(energies_expected)
+            energies_for_plot.append(injected_energies)
+            diffs_for_plot.append(diff)
+            snaps_for_1bplot.append(snap.snapnr)
+
+            # diff should be < 0. Allow for some tolerance here
+            if (diff > energy_conservation_tolerance).any():
+                print(
+                    "Injection Energy Prediction upper boundary is wrong; "
+                    + "snapshot {0:d} tolerance {1:.2e}".format(
+                        snap.snapnr, energy_conservation_tolerance
                     )
-                else:
-                    injected_energies = np.zeros(ngroups) * unyt.erg
-                energies_expected = snap.nstars * emission_rates * dt
-                energies_expected = energies_expected.to(injected_energies.units)
-                diff = np.array(injected_energies / energies_expected - 1.0)
-
-                upper_boundary_for_plot.append(energies_expected)
-                energies_for_plot.append(injected_energies)
-                diffs_for_plot.append(diff)
-                snaps_for_1bplot.append(snap.snapnr)
-
-                # diff should be < 0. Allow for some tolerance here
-                if (diff > float_psi_comparison_tolerance).any():
+                )
+                for g in range(ngroups):
+                    #  if energies_expected[g] > injected_energies[g]:
+                    print("--- group", g + 1)
+                    print("----- injected:", injected_energies[g])
+                    print("----- expected:", energies_expected[g], "should be smaller")
                     print(
-                        "Injection Energy Prediction upper boundary is wrong; "
-                        + "snapshot {0:d} tolerance {1:.2e}".format(
-                            snap.snapnr, float_psi_comparison_tolerance
-                        )
+                        "----- ratio   :", (injected_energies[g] / energies_expected[g])
                     )
-                    for g in range(ngroups):
-                        #  if energies_expected[g] > injected_energies[g]:
-                        print("--- group", g + 1)
-                        print("----- injected:", injected_energies[g])
-                        print(
-                            "----- expected:", energies_expected[g], "should be smaller"
-                        )
-                        print(
-                            "----- ratio   :",
-                            (injected_energies[g] / energies_expected[g]),
-                        )
-                        print("----- diff    :", diff[g], "should be < 0")
-                        found_potential_error = True
+                    print("----- diff    :", diff[g], "should be < 0")
+                    found_potential_error = True
 
-                        if break_on_diff:
-                            quit()
+                    if break_on_diff:
+                        quit()
 
-            if not skip_plots and found_potential_error:
-                # Make this plot if there are possible errors
-                diffs_for_plot = np.array(diffs_for_plot)
-                plt.figure()
-                for g in range(ngroups):
-                    plt.plot(
-                        snaps_for_1bplot,
-                        diffs_for_plot[:, g],
-                        label="group {0:d}".format(g + 1),
-                    )
-                    plt.plot(
-                        [snaps_for_1bplot[0], snaps_for_1bplot[-1]],
-                        [0, 0],
-                        "k",
-                        label="upper boundary",
-                    )
-                plt.legend()
-                plt.xlabel("snapshot")
-                plt.ylabel("injected energy / expected energy - 1")
-                plt.title(
-                    "Difference from expected injected energy - something's fishy"
+        if not skip_plots and found_potential_error:
+            # Make this plot if there are possible errors
+            diffs_for_plot = np.array(diffs_for_plot)
+            plt.figure()
+            for g in range(ngroups):
+                plt.plot(
+                    snaps_for_1bplot,
+                    diffs_for_plot[:, g],
+                    label="group {0:d}".format(g + 1),
                 )
-                plt.show()
-                plt.close()
+            plt.plot(
+                [snaps_for_1bplot[0], snaps_for_1bplot[-1]],
+                [0, 0],
+                "k",
+                label="upper boundary",
+            )
+            plt.legend()
+            plt.xlabel("snapshot")
+            plt.ylabel("injected energy / expected energy - 1")
+            plt.title("Difference from expected injected energy - something's fishy")
+            plt.show()
+            plt.close()
 
     # --------------------------------
     # Create additional plots?
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks.py b/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks.py
index f28dcc661d5d7dad9b97cfc6c89f60658c1fae1d..bd66f11ff9892a103fa84d3d9bcb45e27991f052 100755
--- a/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks.py
+++ b/examples/RadiativeTransferTests/UniformBox_3D/rt_sanity_checks.py
@@ -56,18 +56,20 @@ else:
     file_prefix = "output"
 
 
-def check_hydro_sanity(snapdata):
+def check_hydro_sanity(snapdata, rundata):
     """
     Sanity checks for hydro variables.
     - injection always done?
     - gradients always done?
     - thermochemistry always done?
     - RT transport calls >= RT gradient calls?
+    - number of subcycles != 0?
     """
 
     npart = snapdata[0].gas.coords.shape[0]
 
     print("Checking hydro")
+    warning_printed = False
 
     # ----------------------------------------------
     # check absolute values of every snapshot
@@ -186,8 +188,17 @@ def check_hydro_sanity(snapdata):
         # at least the number of calls to transport interactions
         # in RT interactions
         # --------------------------------------------------------------
-        fishy = gas.RTCallsIactTransportInteraction < gas.RTCallsIactGradientInteraction
-        if fishy.any():
+        if rundata.with_mpi:
+            check = False
+            if not warning_printed:
+                print("- MPI run: skipping hydro sanity interaction call count checks")
+                warning_printed = True
+        else:
+            fishy = (
+                gas.RTCallsIactTransportInteraction < gas.RTCallsIactGradientInteraction
+            )
+            check = fishy.any()
+        if check:
             print("- checking hydro sanity pt2.5; snapshot", snap.snapnr)
             print(
                 "--- Found RT transport calls iact < gradient calls iact:",
@@ -207,10 +218,25 @@ def check_hydro_sanity(snapdata):
             if break_on_diff:
                 quit()
 
+        # -------------------------------------------------------------
+        # Check that the subcycle counter isn't zero.
+        # We expect a particle to be radioactive at least each time it
+        # is hydro active, so the subcycle counter must never be zero.
+        # -------------------------------------------------------------
+
+        fishy = gas.nsubcycles <= 0
+        if fishy.any():
+            print("- checking hydro sanity pt 2.6; snapshot", snap.snapnr)
+            print("Found nsubcycles <= 0:", np.count_nonzero(fishy), "/", npart)
+            if print_diffs:
+                print("nsubcycles:", gas.nsubcycles[fishy])
+            if break_on_diff:
+                quit()
+
     return
 
 
-def check_stars_sanity(snapdata):
+def check_stars_sanity(snapdata, rundata):
     """
     Sanity checks for stars variables.
     - total calls keep increasing?
@@ -250,7 +276,7 @@ def check_stars_sanity(snapdata):
     return
 
 
-def check_stars_hydro_interaction_sanity(snapdata):
+def check_stars_hydro_interaction_sanity(snapdata, rundata):
     """
     Sanity checks for hydro vs star interaction
     call counts.
@@ -329,9 +355,9 @@ def main():
         prefix=file_prefix, skip_snap_zero=skip_snap_zero, skip_last_snap=skip_last_snap
     )
 
-    check_hydro_sanity(snapdata)
-    check_stars_sanity(snapdata)
-    check_stars_hydro_interaction_sanity(snapdata)
+    check_hydro_sanity(snapdata, rundata)
+    check_stars_sanity(snapdata, rundata)
+    check_stars_hydro_interaction_sanity(snapdata, rundata)
 
     return
 
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/rt_uniform_box_checks.py b/examples/RadiativeTransferTests/UniformBox_3D/rt_uniform_box_checks.py
index c8a16ed9ed82d05d337ff000881a98149efa155f..49c7a0cf6295b06be192e76b206823597740bf0a 100755
--- a/examples/RadiativeTransferTests/UniformBox_3D/rt_uniform_box_checks.py
+++ b/examples/RadiativeTransferTests/UniformBox_3D/rt_uniform_box_checks.py
@@ -222,6 +222,25 @@ def check_all_hydro_is_equal(snapdata):
         if (compare.gas.ThermochemistryDone[nzs] == 0).any():
             print("Oh no 3")
 
+        # ---------------------------------------------------------------
+        # Check numbers of subcycles.
+        # ---------------------------------------------------------------
+        fishy = ref.gas.nsubcycles != compare.gas.nsubcycles
+        if fishy.any():
+            print("- Comparing hydro", ref.snapnr, "->", compare.snapnr)
+            print(
+                "--- Subcycle Calls count differ: {0:8d} / {1:8d}; ".format(
+                    np.count_nonzero(fishy), npart
+                )
+            )
+            if not skip_last_snap:
+                print(
+                    "Note, this might be acceptable behaviour for the final snapshot. You currently aren't skipping it in this check."
+                )
+
+            if break_on_diff:
+                quit()
+
     return
 
 
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/run.sh b/examples/RadiativeTransferTests/UniformBox_3D/run.sh
index 9f81af658a3af253ce1faf407ac39b4cdffe0867..214533a4f6366f76ad19ec57c1c1effb80054c8b 100755
--- a/examples/RadiativeTransferTests/UniformBox_3D/run.sh
+++ b/examples/RadiativeTransferTests/UniformBox_3D/run.sh
@@ -9,12 +9,26 @@ if [ ! -f 'uniformBox-rt.hdf5' ]; then
     python3 makeIC.py
 fi
 
+# use cmdline args as shortcut to run with debugger/MPI
+# -g run with gdb
+# -m run with MPI
+# -mg run with MPI and gdb in individual xterm windows
+# -ml run with MPI and write individual output file per MPI rank
 cmd=../../../swift
 if [ $# -gt 0 ]; then
     case "$1" in 
-    g | gdb)
+    -g | g | gdb)
         cmd='gdb --args ../../../swift'
         ;;
+    -m | m | mpi)
+        cmd='mpirun -n 2 ../../../swift_mpi' 
+        ;;
+    -mg | -gm | gm | mg | gmpi | gdbmpi )
+        cmd='mpirun -n 2 xterm -e gdb -ex run --args ../../../swift_mpi'
+        ;;
+    -ml | ml | lm | mpilog | logmpi)
+        cmd='mpirun -n 2 --output-filename individual_rank_output --merge-stderr-to-stdout ../../../swift_mpi'
+        ;;
     *)
         echo unknown cmdline param, running without gdb
         ;;
@@ -24,8 +38,8 @@ fi
 # Run SWIFT with RT
 $cmd \
     --hydro --threads=4 --stars --external-gravity \
-    --feedback --radiation \
-    uniform_rt_timestep_output_sync.yml 2>&1 | tee output.log
+    --feedback --radiation --verbose=0 \
+    uniform_rt_timestep_output_sync.yml  2>&1 | tee output.log
 
 echo "running sanity checks"
 python3 ./rt_sanity_checks.py | tee sanity_check.log
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py b/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py
index 5d07d105c9ef5f1905a6ce507287b05cdd96713b..2a7c12325d69c288f7c6f8fe468aca55df937886 100644
--- a/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py
+++ b/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_GEAR_io.py
@@ -104,6 +104,8 @@ class Rundata(object):
         self.const_emission_rates = None
         self.reduced_speed_of_light = -1.0
 
+        self.with_mpi = False
+
         return
 
 
@@ -220,6 +222,11 @@ def get_snap_data(prefix="output", skip_snap_zero=False, skip_last_snap=False):
 
     rundata.reduced_speed_of_light = firstfile.metadata.reduced_lightspeed
 
+    with_mpi = False
+    if firstfile.metadata.code["MPI library"] != b"Non-MPI version of SWIFT":
+        with_mpi = True
+    rundata.with_mpi = with_mpi
+
     # -------------------
     # Read in all files
     # -------------------
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_debug_io.py b/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_debug_io.py
index c68f9bd4dce8d9ba29a753ab381bd0b406f7168f..ddeb19895069f4a2396c2bce80950476a48acc99 100644
--- a/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_debug_io.py
+++ b/examples/RadiativeTransferTests/UniformBox_3D/swift_rt_debug_io.py
@@ -50,7 +50,8 @@ class RTGasData(object):
         self.GradientsDone = None
 
         self.RadiationAbsorbedTot = None
-        self.InjectPrepCountsTot = None
+
+        self.nsubcycles = None
 
         return
 
@@ -95,6 +96,7 @@ class Rundata(object):
 
     def __init__(self):
         self.has_stars = False  # assume we don't have stars, check while reading in
+        self.with_mpi = False
 
         return
 
@@ -166,6 +168,12 @@ def get_snap_data(prefix="output", skip_snap_zero=False, skip_last_snap=False):
             "Compile swift --with-rt=debug",
         )
 
+    with_mpi = False
+    mpistr = F["Code"].attrs["MPI library"]
+    if mpistr != b"Non-MPI version of SWIFT":
+        with_mpi = True
+    rundata.with_mpi = with_mpi
+
     F.close()
 
     for f in hdf5files:
@@ -203,6 +211,7 @@ def get_snap_data(prefix="output", skip_snap_zero=False, skip_last_snap=False):
         newsnap.gas.ThermochemistryDone = Gas["RTDebugThermochemistryDone"][:][inds]
 
         newsnap.gas.RadiationAbsorbedTot = Gas["RTDebugRadAbsorbedTot"][:][inds]
+        newsnap.gas.nsubcycles = Gas["RTDebugSubcycles"][:][inds]
 
         has_stars = False
         try:
@@ -230,8 +239,8 @@ def get_snap_data(prefix="output", skip_snap_zero=False, skip_last_snap=False):
         rundata.has_stars = rundata.has_stars or snap.has_stars
 
     if len(snapdata) == 0:
-        print(
-            "Didn't read in snapshot data. Do you only have 2 snapshots in total and skipping the first and the last?"
-        )
+        print("Didn't read in snapshot data.")
+        print("Do you only have 2 snapshots and are skipping the first and the last?")
+        quit()
 
     return snapdata, rundata
diff --git a/examples/RadiativeTransferTests/UniformBox_3D/uniform_rt_timestep_output_sync.yml b/examples/RadiativeTransferTests/UniformBox_3D/uniform_rt_timestep_output_sync.yml
index f7dbe1e09fa33d6e52b0a6d1dedab38eec982ffd..5c04f0a493ee5631947bbb409fe4dfa848a646bc 100644
--- a/examples/RadiativeTransferTests/UniformBox_3D/uniform_rt_timestep_output_sync.yml
+++ b/examples/RadiativeTransferTests/UniformBox_3D/uniform_rt_timestep_output_sync.yml
@@ -15,6 +15,7 @@ TimeIntegration:
   time_end:   9.536742e-07
   dt_min:     1.e-12   # The minimal time-step size of the simulation (in internal units).
   dt_max:     5.e-8   # The maximal time-step size of the simulation (in internal units).
+  max_nr_rt_subcycles: 4
 
 # Parameters governing the snapshots
 Snapshots:
diff --git a/examples/parameter_example.yml b/examples/parameter_example.yml
index aa82405bfefb0307100326e4ea1149421fa97e80..630ee08e1adc77ad8b6ec9925ede73467ddc327b 100644
--- a/examples/parameter_example.yml
+++ b/examples/parameter_example.yml
@@ -160,6 +160,7 @@ TimeIntegration:
   dt_max:              1e-2  # The maximal time-step size of the simulation (in internal units).
   max_dt_RMS_factor:   0.25  # (Optional) Dimensionless factor for the maximal displacement allowed based on the RMS velocities.
   dt_RMS_use_gas_only: 0     # (Optional) When computing the max RMS dt, should only the gas particles be considered in the baryon component calculation?
+  max_nr_rt_subcycles: 0     # (Optional) Maximal number of radiative transfer sub-cycles per hydro step for any particle. Set = 0 to disable subcycling. Needs to be a power of 2.
   
 # Parameters governing the snapshots
 Snapshots:
diff --git a/src/Makefile.am b/src/Makefile.am
index 3dbb2cfbf4f7d65d33b2034c73d376808c9fee7e..9f2c5a4934480d89905fc6983ffa31659513556d 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -42,7 +42,7 @@ endif
 
 # List required headers
 include_HEADERS = space.h runner.h queue.h task.h lock.h cell.h part.h const.h 
-include_HEADERS += cell_hydro.h cell_stars.h cell_grav.h cell_sinks.h cell_black_holes.h 
+include_HEADERS += cell_hydro.h cell_stars.h cell_grav.h cell_sinks.h cell_black_holes.h cell_rt.h
 include_HEADERS += engine.h swift.h serial_io.h timers.h debug.h scheduler.h proxy.h parallel_io.h 
 include_HEADERS += common_io.h single_io.h distributed_io.h map.h tools.h  partition_fixed_costs.h 
 include_HEADERS += partition.h clocks.h parser.h physical_constants.h physical_constants_cgs.h potential.h version.h 
diff --git a/src/active.h b/src/active.h
index daee44ab55e73b40aacf718b8165228a8c985f3b..bbdedf77d79bd9816849e4ce555a63c7e0833395 100644
--- a/src/active.h
+++ b/src/active.h
@@ -144,6 +144,26 @@ __attribute__((always_inline)) INLINE static int cell_are_bpart_drifted(
   return (c->black_holes.ti_old_part == e->ti_current);
 }
 
+/**
+ * @brief Check that the #part in a #cell have been drifted to the current time.
+ * This is just a prototype function to keep the iact functions clean. As we
+ * don't care about the drifts during the RT sub-cycling, this always just
+ * returns true.
+ *
+ * @param c The #cell.
+ * @param e The #engine containing information about the current time.
+ * @return 1 if the #cell has been drifted to the current time, 0 otherwise.
+ */
+__attribute__((always_inline)) INLINE static int
+cell_are_part_drifted_rt_sub_cycle(const struct cell *c,
+                                   const struct engine *e) {
+
+  /* Note: we can't just use "cell_are_part_drifted" in the hydro_iact
+   * functions, because an RT sub-cycle may be called during a main
+   * step for a cell that is hydro inactive and thus may be not drifted. */
+  return 1;
+}
+
 /* Are cells / particles active for regular tasks ? */
 
 /**
@@ -168,6 +188,44 @@ __attribute__((always_inline)) INLINE static int cell_is_active_hydro(
   return (c->hydro.ti_end_min == e->ti_current);
 }
 
+/**
+ * @brief Does a cell contain any particle finishing their RT time-step now ?
+ *
+ * @param c The #cell.
+ * @param e The #engine containing information about the current time.
+ * @return 1 if the #cell contains at least an active particle, 0 otherwise.
+ */
+__attribute__((always_inline)) INLINE static int cell_is_rt_active(
+    struct cell *c, const struct engine *e) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+  /* Each cell doing RT needs to have the rt_advance_cell_time task.
+   * If it doesn't, it's not doing RT. This can happen for foreign
+   * cells which have been sent to a foreign node for other interactions,
+   * e.g. gravity. However, foreign cells may have tasks on levels below
+   * the rt_advance_cell_time, so allow for that exception in this check. */
+
+  int has_rt_advance_cell_time = 0;
+  if (c->super != NULL)
+    has_rt_advance_cell_time = c->super->rt.rt_advance_cell_time != NULL;
+
+  if (has_rt_advance_cell_time &&
+      (c->rt.ti_rt_end_min < e->ti_current_subcycle)) {
+    error(
+        "cell %lld in an impossible time-zone! c->ti_rt_end_min=%lld (t=%e) "
+        "and e->ti_current=%lld (t=%e, a=%e) c->nodeID=%d ACT=%d count=%d",
+        c->cellID, c->rt.ti_rt_end_min, c->rt.ti_rt_end_min * e->time_base,
+        e->ti_current_subcycle, e->ti_current_subcycle * e->time_base,
+        e->cosmology->a, c->nodeID, c->rt.rt_advance_cell_time != NULL,
+        c->hydro.count);
+  }
+#endif
+
+  /* If there are no sub-cycles, e->ti_current_subcycle = e->ti_current.
+   * This is also the case if we're currently doing a normal SWIFT step. */
+  return (c->rt.ti_rt_end_min == e->ti_current_subcycle);
+}
+
 /**
  * @brief Does a cell contain any g-particle finishing their time-step now ?
  *
@@ -323,6 +381,33 @@ __attribute__((always_inline)) INLINE static int part_is_active_no_debug(
   return (part_bin <= max_active_bin);
 }
 
+/**
+ * @brief Is this particle finishing its RT time-step now ?
+ *
+ * @param p The #part.
+ * @param e The #engine containing information about the current time.
+ * @return 1 if the #part is active, 0 otherwise.
+ */
+__attribute__((always_inline)) INLINE static int part_is_rt_active(
+    const struct part *p, const struct engine *e) {
+
+  const timebin_t max_active_bin = e->max_active_bin_subcycle;
+  const timebin_t part_bin = p->rt_time_data.time_bin;
+
+#ifdef SWIFT_DEBUG_CHECKS
+  const integertime_t ti_current_subcycle = e->ti_current_subcycle;
+  const integertime_t ti_end =
+      get_integer_time_end(ti_current_subcycle, p->rt_time_data.time_bin);
+  if (ti_end < ti_current_subcycle)
+    error(
+        "particle in an impossible time-zone! p->ti_end_subcycle=%lld "
+        "e->ti_current_subcycle=%lld",
+        ti_end, ti_current_subcycle);
+#endif
+
+  return (part_bin <= max_active_bin);
+}
+
 /**
  * @brief Is this g-particle finishing its time-step now ?
  *
diff --git a/src/cell.c b/src/cell.c
index 73a82315c180ba4ff0af24ec97315b6a1665b363..c8d5dac7271b6e9f557aa4940c30020d367a3040 100644
--- a/src/cell.c
+++ b/src/cell.c
@@ -579,8 +579,8 @@ void cell_clean_links(struct cell *c, void *data) {
   c->hydro.gradient = NULL;
   c->hydro.force = NULL;
   c->hydro.limiter = NULL;
-  c->hydro.rt_gradient = NULL;
-  c->hydro.rt_transport = NULL;
+  c->rt.rt_gradient = NULL;
+  c->rt.rt_transport = NULL;
   c->grav.grav = NULL;
   c->grav.mm = NULL;
   c->stars.density = NULL;
@@ -1253,7 +1253,7 @@ void cell_check_timesteps(const struct cell *c, const integertime_t ti_current,
 
   if (c->hydro.ti_end_min == 0 && c->grav.ti_end_min == 0 &&
       c->stars.ti_end_min == 0 && c->black_holes.ti_end_min == 0 &&
-      c->sinks.ti_end_min == 0 && c->nr_tasks > 0)
+      c->sinks.ti_end_min == 0 && c->rt.ti_rt_end_min == 0 && c->nr_tasks > 0)
     error("Cell without assigned time-step");
 
   if (c->split) {
diff --git a/src/cell.h b/src/cell.h
index da6c610c732d9dec745ca3bce1cec36372f52778..a5c776b7303ac964881acbd8dafa7d48ef9bd894 100644
--- a/src/cell.h
+++ b/src/cell.h
@@ -36,6 +36,7 @@
 #include "cell_black_holes.h"
 #include "cell_grav.h"
 #include "cell_hydro.h"
+#include "cell_rt.h"
 #include "cell_sinks.h"
 #include "cell_stars.h"
 #include "ghost_stats.h"
@@ -210,6 +211,17 @@ struct pcell {
 
   } sinks;
 
+  /*! RT variables */
+  struct {
+
+    /*! Minimal integer end-of-timestep in this cell for RT tasks */
+    integertime_t ti_rt_end_min;
+
+    /*! smallest RT time-step size in this cell */
+    integertime_t ti_rt_min_step_size;
+
+  } rt;
+
   /*! Maximal depth in that part of the tree */
   int maxdepth;
 
@@ -260,6 +272,16 @@ struct pcell_step {
     /*! Maximal distance any #part has travelled since last rebuild */
     float dx_max_part;
   } black_holes;
+
+  struct {
+
+    /*! Minimal integer end-of-timestep in this cell (rt) */
+    integertime_t ti_rt_end_min;
+
+    /*! smallest RT time-step size in this cell */
+    integertime_t ti_rt_min_step_size;
+
+  } rt;
 };
 
 /**
@@ -318,7 +340,10 @@ enum cell_flags {
   cell_flag_do_hydro_sync = (1UL << 17),
   cell_flag_do_hydro_sub_sync = (1UL << 18),
   cell_flag_unskip_self_grav_processed = (1UL << 19),
-  cell_flag_unskip_pair_grav_processed = (1UL << 20)
+  cell_flag_unskip_pair_grav_processed = (1UL << 20),
+  cell_flag_skip_rt_sort = (1UL << 21),    /* skip rt_sort after a RT recv? */
+  cell_flag_do_rt_sub_sort = (1UL << 22),  /* same as hydro_sub_sort for RT */
+  cell_flag_rt_requests_sort = (1UL << 23) /* was this sort requested by RT? */
 };
 
 /**
@@ -370,6 +395,9 @@ struct cell {
   /*! Sink particles variables */
   struct cell_sinks sinks;
 
+  /*! Radiative transfer variables */
+  struct cell_rt rt;
+
 #ifdef WITH_MPI
   /*! MPI variables */
   struct {
@@ -540,7 +568,8 @@ int cell_unskip_stars_tasks(struct cell *c, struct scheduler *s,
                             const int with_star_formation,
                             const int with_star_formation_sink);
 int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s);
-int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s);
+int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s,
+                         const int sub_cycle);
 int cell_unskip_black_holes_tasks(struct cell *c, struct scheduler *s);
 int cell_unskip_gravity_tasks(struct cell *c, struct scheduler *s);
 void cell_drift_part(struct cell *c, const struct engine *e, int force,
@@ -582,6 +611,9 @@ void cell_activate_subcell_black_holes_tasks(struct cell *ci, struct cell *cj,
                                              const int with_timestep_sync);
 void cell_activate_subcell_external_grav_tasks(struct cell *ci,
                                                struct scheduler *s);
+void cell_activate_subcell_rt_tasks(struct cell *ci, struct cell *cj,
+                                    struct scheduler *s, const int sub_cycle);
+void cell_set_no_rt_sort_flag_up(struct cell *c);
 void cell_activate_super_spart_drifts(struct cell *c, struct scheduler *s);
 void cell_activate_super_sink_drifts(struct cell *c, struct scheduler *s);
 void cell_activate_drift_part(struct cell *c, struct scheduler *s);
@@ -590,6 +622,7 @@ void cell_activate_drift_spart(struct cell *c, struct scheduler *s);
 void cell_activate_drift_sink(struct cell *c, struct scheduler *s);
 void cell_activate_drift_bpart(struct cell *c, struct scheduler *s);
 void cell_activate_sync_part(struct cell *c, struct scheduler *s);
+void cell_activate_rt_sorts(struct cell *c, int sid, struct scheduler *s);
 void cell_activate_hydro_sorts(struct cell *c, int sid, struct scheduler *s);
 void cell_activate_stars_sorts(struct cell *c, int sid, struct scheduler *s);
 void cell_activate_limiter(struct cell *c, struct scheduler *s);
diff --git a/src/cell_convert_part.c b/src/cell_convert_part.c
index cf2ee12834fffa78c06e20c434f9cc762ab5dd8e..78f430a07c3c91150fc87482edb4235108f46363 100644
--- a/src/cell_convert_part.c
+++ b/src/cell_convert_part.c
@@ -565,6 +565,9 @@ void cell_remove_part(const struct engine *e, struct cell *c, struct part *p,
 
   /* Mark the particle as inhibited */
   p->time_bin = time_bin_inhibited;
+  /* Mark the RT time bin as inhibited as well,
+   * so part_is_rt_active() checks work as intended */
+  p->rt_time_data.time_bin = time_bin_inhibited;
 
   /* Mark the gpart as inhibited and stand-alone */
   if (p->gpart) {
diff --git a/src/cell_hydro.h b/src/cell_hydro.h
index 78317797ce9eb1ef20c4e6b96d62d21bf0db51f0..39db7bc21934e434583551de2a693da46fc7cacc 100644
--- a/src/cell_hydro.h
+++ b/src/cell_hydro.h
@@ -103,38 +103,6 @@ struct cell_hydro {
     /*! Task for sorting the stars again after a SF event */
     struct task *stars_resort;
 
-#ifdef RT_NONE
-    union {
-#endif
-
-      /*! Radiative transfer ghost in task */
-      struct task *rt_in;
-
-      /*! Radiative transfer ghost1 task (finishes up injection) */
-      struct task *rt_ghost1;
-
-      /*! Task for self/pair gradient step of radiative transfer */
-      struct link *rt_gradient;
-
-      /*! Radiative transfer ghost2 task */
-      struct task *rt_ghost2;
-
-      /*! Task for self/pair transport step of radiative transfer */
-      struct link *rt_transport;
-
-      /*! Radiative transfer transport out task */
-      struct task *rt_transport_out;
-
-      /*! Radiative transfer thermochemistry task */
-      struct task *rt_tchem;
-
-      /*! Radiative transfer ghost out task */
-      struct task *rt_out;
-
-#ifdef RT_NONE
-    };
-#endif
-
     /*! Last (integer) time the cell's part were drifted forward in time. */
     integertime_t ti_old_part;
 
diff --git a/src/cell_pack.c b/src/cell_pack.c
index 80bedf82571845bd76cea3ea2a49e44272cfc527..34dbf83c1671a0b4f9161e21cedc5545f1b2e342 100644
--- a/src/cell_pack.c
+++ b/src/cell_pack.c
@@ -51,6 +51,8 @@ int cell_pack(struct cell *restrict c, struct pcell *restrict pc,
   pc->stars.ti_end_min = c->stars.ti_end_min;
   pc->sinks.ti_end_min = c->sinks.ti_end_min;
   pc->black_holes.ti_end_min = c->black_holes.ti_end_min;
+  pc->rt.ti_rt_end_min = c->rt.ti_rt_end_min;
+  pc->rt.ti_rt_min_step_size = c->rt.ti_rt_min_step_size;
 
   pc->hydro.ti_old_part = c->hydro.ti_old_part;
   pc->grav.ti_old_part = c->grav.ti_old_part;
@@ -208,6 +210,8 @@ int cell_unpack(struct pcell *restrict pc, struct cell *restrict c,
   c->stars.ti_end_min = pc->stars.ti_end_min;
   c->black_holes.ti_end_min = pc->black_holes.ti_end_min;
   c->sinks.ti_end_min = pc->sinks.ti_end_min;
+  c->rt.ti_rt_end_min = pc->rt.ti_rt_end_min;
+  c->rt.ti_rt_min_step_size = pc->rt.ti_rt_min_step_size;
 
   c->hydro.ti_old_part = pc->hydro.ti_old_part;
   c->grav.ti_old_part = pc->grav.ti_old_part;
@@ -341,6 +345,8 @@ int cell_pack_end_step(const struct cell *c, struct pcell_step *pcells) {
   /* Pack this cell's data. */
   pcells[0].hydro.ti_end_min = c->hydro.ti_end_min;
   pcells[0].hydro.dx_max_part = c->hydro.dx_max_part;
+  pcells[0].rt.ti_rt_end_min = c->rt.ti_rt_end_min;
+  pcells[0].rt.ti_rt_min_step_size = c->rt.ti_rt_min_step_size;
 
   pcells[0].grav.ti_end_min = c->grav.ti_end_min;
 
@@ -383,6 +389,9 @@ int cell_unpack_end_step(struct cell *c, const struct pcell_step *pcells) {
   c->hydro.ti_end_min = pcells[0].hydro.ti_end_min;
   c->hydro.dx_max_part = pcells[0].hydro.dx_max_part;
 
+  c->rt.ti_rt_end_min = pcells[0].rt.ti_rt_end_min;
+  c->rt.ti_rt_min_step_size = pcells[0].rt.ti_rt_min_step_size;
+
   c->grav.ti_end_min = pcells[0].grav.ti_end_min;
 
   c->stars.ti_end_min = pcells[0].stars.ti_end_min;
diff --git a/src/cell_rt.h b/src/cell_rt.h
new file mode 100644
index 0000000000000000000000000000000000000000..7ccd7795381d96d07bed7c69e0531efbdbba6c87
--- /dev/null
+++ b/src/cell_rt.h
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+#ifndef SWIFT_CELL_RT_H
+#define SWIFT_CELL_RT_H
+
+/* Config parameters. */
+#include "../config.h"
+
+/* Local includes. */
+#include "timeline.h"
+
+/**
+ * @brief Radiative transfer related cell variables.
+ */
+struct cell_rt {
+
+  /* If we are not using RT, compact as much of the unecessary variables
+     into an anonymous union to save memory in the cell structure. */
+#ifdef RT_NONE
+  union {
+#endif
+
+    /*! Radiative transfer ghost in task */
+    struct task *rt_in;
+
+    /*! Radiative transfer ghost1 task (finishes up injection) */
+    struct task *rt_ghost1;
+
+    /*! Task for self/pair gradient step of radiative transfer */
+    struct link *rt_gradient;
+
+    /*! Radiative transfer ghost2 task */
+    struct task *rt_ghost2;
+
+    /*! Task for self/pair transport step of radiative transfer */
+    struct link *rt_transport;
+
+    /*! Radiative transfer transport out task */
+    struct task *rt_transport_out;
+
+    /*! Radiative transfer thermochemistry task */
+    struct task *rt_tchem;
+
+    /*! Radiative transfer cell time advancement task */
+    struct task *rt_advance_cell_time;
+
+    /*! Sort a cell after a recv rt gradients */
+    struct task *rt_sorts;
+
+    /*! Collect the cell times from the super to the top level */
+    struct task *rt_collect_times;
+
+    /*! Radiative transfer ghost out task */
+    struct task *rt_out;
+
+    /*! Bit mask of sorts that need to be computed for this cell.
+     * Needed to be able to skip sorting undrifted cells. */
+    uint16_t do_sort;
+
+#ifdef RT_NONE
+  };
+#endif
+
+#ifdef SWIFT_RT_DEBUG_CHECKS
+  /*! has rt_advance_cell_time run on this cell? */
+  int advanced_time;
+#endif
+
+  /*! Minimum end of (integer) time step in this cell for RT tasks. */
+  integertime_t ti_rt_end_min;
+
+  /*! Maximum beginning of (integer) time step in this cell for RT tasks. */
+  integertime_t ti_rt_beg_max;
+
+  /*! Minimum (integer) time step size in this cell for RT tasks. */
+  integertime_t ti_rt_min_step_size;
+
+  /*! Number of #part updated for RT in this cell */
+  int updated;
+};
+
+#endif /* SWIFT_CELL_RT_H */
diff --git a/src/cell_unskip.c b/src/cell_unskip.c
index f90bc100781e828e66013dc1dbcf78c94c4d342f..d41327b1575eb32462eb95f8fb023af9afa76572 100644
--- a/src/cell_unskip.c
+++ b/src/cell_unskip.c
@@ -594,6 +594,7 @@ void cell_activate_hydro_sorts_up(struct cell *c, struct scheduler *s) {
       error("Trying to activate un-existing c->hydro.sorts");
 #endif
     scheduler_activate(s, c->hydro.sorts);
+    cell_set_flag(c, cell_flag_skip_rt_sort);
     if (c->nodeID == engine_rank) cell_activate_drift_part(c, s);
   } else {
     for (struct cell *parent = c->parent;
@@ -606,6 +607,7 @@ void cell_activate_hydro_sorts_up(struct cell *c, struct scheduler *s) {
           error("Trying to activate un-existing parents->hydro.sorts");
 #endif
         scheduler_activate(s, parent->hydro.sorts);
+        cell_set_flag(parent, cell_flag_skip_rt_sort);
         if (parent->nodeID == engine_rank) cell_activate_drift_part(parent, s);
         break;
       }
@@ -636,6 +638,95 @@ void cell_activate_hydro_sorts(struct cell *c, int sid, struct scheduler *s) {
   }
 }
 
+/**
+ * @brief Activate the sorts up a cell hierarchy. Activate drifts
+ * and hydro sorts on local cells, and rt_sorts on foreign cells.
+ */
+void cell_activate_rt_sorts_up(struct cell *c, struct scheduler *s) {
+
+  cell_set_flag(c, cell_flag_rt_requests_sort);
+
+  if (c == c->hydro.super) {
+#ifdef SWIFT_DEBUG_CHECKS
+    if (c->nodeID == engine_rank && c->hydro.sorts == NULL)
+      error("Trying to activate non-existing c->hydro.sorts");
+    if (c->nodeID != engine_rank && c->rt.rt_sorts == NULL)
+      error("Trying to activate non-existing c->rt.rt_sorts");
+#endif
+    if (c->nodeID == engine_rank) {
+      cell_set_flag(c, cell_flag_skip_rt_sort);
+      scheduler_activate(s, c->hydro.sorts);
+    } else {
+      scheduler_activate(s, c->rt.rt_sorts);
+    }
+  } else {
+
+    for (struct cell *parent = c->parent;
+         parent != NULL && !cell_get_flag(parent, cell_flag_do_rt_sub_sort);
+         parent = parent->parent) {
+
+      /* Need a separate flag for RT sub sorts because RT sorts don't
+       * necessarily activate the hydro sorts tasks, yet the do_hydro_sub_sort
+       * flag is used as an early exit while climbing up the tree. */
+      cell_set_flag(parent, cell_flag_do_rt_sub_sort);
+      cell_set_flag(parent, cell_flag_rt_requests_sort);
+
+      if (parent == c->hydro.super) {
+
+#ifdef SWIFT_DEBUG_CHECKS
+        if (parent->nodeID == engine_rank && parent->hydro.sorts == NULL)
+          error("Trying to activate non-existing parents->hydro.sorts");
+        if (parent->nodeID != engine_rank && parent->rt.rt_sorts == NULL)
+          error("Trying to activate non-existing parents->rt.rt_sorts");
+#endif
+
+        if (parent->nodeID == engine_rank) {
+          /* Mark the progeny to skip the RT sort as well */
+          cell_set_flag(c, cell_flag_skip_rt_sort);
+          scheduler_activate(s, parent->hydro.sorts);
+        } else {
+          scheduler_activate(s, parent->rt.rt_sorts);
+        }
+        break;
+      }
+    }
+  }
+}
+
+/**
+ * @brief Activate the sorts on a given cell, if needed. Activate
+ * hydro sorts on local cells, and rt_sorts on foreign cells.
+ */
+void cell_activate_rt_sorts(struct cell *c, int sid, struct scheduler *s) {
+  /* Do we need to re-sort? */
+  if (c->hydro.dx_max_sort > space_maxreldx * c->dmin) {
+    /* Climb up the tree to active the sorts in that direction */
+    for (struct cell *finger = c; finger != NULL; finger = finger->parent) {
+      if (finger->hydro.requires_sorts) {
+        atomic_or(&finger->hydro.do_sort, finger->hydro.requires_sorts);
+        cell_activate_rt_sorts_up(finger, s);
+      }
+      finger->hydro.sorted = 0;
+    }
+  }
+
+  /* Has this cell been sorted at all for the given sid? */
+  if (!(c->hydro.sorted & (1 << sid)) || c->nodeID != engine_rank) {
+    atomic_or(&c->hydro.do_sort, (1 << sid));
+    cell_activate_rt_sorts_up(c, s);
+  }
+}
+
+/**
+ * @brief Mark cells up a hierarchy to not run RT sorts.
+ * */
+void cell_set_skip_rt_sort_flag_up(struct cell *c) {
+
+  for (struct cell *finger = c; finger != NULL; finger = finger->parent) {
+    cell_set_flag(finger, cell_flag_skip_rt_sort);
+  }
+}
+
 /**
  * @brief Activate the sorts up a cell hierarchy.
  */
@@ -1431,6 +1522,94 @@ void cell_activate_subcell_external_grav_tasks(struct cell *ci,
   }
 }
 
+/**
+ * @brief Traverse a sub-cell task and activate the sort tasks that are
+ * required by a RT task
+ *
+ * @param ci The first #cell we recurse in.
+ * @param cj The second #cell we recurse in.
+ * @param s The task #scheduler.
+ * @param sub_cycle Are we in a subcycle or not?
+ */
+void cell_activate_subcell_rt_tasks(struct cell *ci, struct cell *cj,
+                                    struct scheduler *s, const int sub_cycle) {
+
+  /* Only do this during real time steps, not during subcycling. */
+  if (sub_cycle) return;
+  const struct engine *e = s->space->e;
+
+  /* Store the current dx_max and h_max values. */
+  ci->hydro.dx_max_part_old = ci->hydro.dx_max_part;
+  ci->hydro.h_max_old = ci->hydro.h_max;
+
+  if (cj != NULL) {
+    cj->hydro.dx_max_part_old = cj->hydro.dx_max_part;
+    cj->hydro.h_max_old = cj->hydro.h_max;
+  }
+
+  const int ci_active = cell_is_rt_active(ci, e);
+  const int cj_active = ((cj != NULL) && cell_is_rt_active(cj, e));
+
+  /* Self interaction? */
+  if (cj == NULL) {
+    /* Do anything? */
+    if (ci->hydro.count == 0 || !ci_active) return;
+
+    /* Recurse? */
+    if (cell_can_recurse_in_self_hydro_task(ci)) {
+      /* Loop over all progenies and pairs of progenies */
+      for (int j = 0; j < 8; j++) {
+        if (ci->progeny[j] != NULL) {
+          cell_activate_subcell_rt_tasks(ci->progeny[j], NULL, s, sub_cycle);
+          for (int k = j + 1; k < 8; k++)
+            if (ci->progeny[k] != NULL)
+              cell_activate_subcell_rt_tasks(ci->progeny[j], ci->progeny[k], s,
+                                             sub_cycle);
+        }
+      }
+    }
+  }
+
+  /* Otherwise, pair interation */
+  else {
+
+    /* Should we even bother? */
+    if (!ci_active && !cj_active) return;
+    if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
+
+    /* Get the orientation of the pair. */
+    double shift[3];
+    const int sid = space_getsid(s->space, &ci, &cj, shift);
+
+    /* recurse? */
+    if (cell_can_recurse_in_pair_hydro_task(ci) &&
+        cell_can_recurse_in_pair_hydro_task(cj)) {
+      const struct cell_split_pair *csp = &cell_split_pairs[sid];
+      for (int k = 0; k < csp->count; k++) {
+        const int pid = csp->pairs[k].pid;
+        const int pjd = csp->pairs[k].pjd;
+        if (ci->progeny[pid] != NULL && cj->progeny[pjd] != NULL)
+          cell_activate_subcell_rt_tasks(ci->progeny[pid], cj->progeny[pjd], s,
+                                         sub_cycle);
+      }
+    }
+
+    /* Otherwise, activate the sorts and drifts. */
+    else if (ci_active || cj_active) {
+
+      /* We are going to interact this pair, so store some values. */
+      atomic_or(&ci->hydro.requires_sorts, 1 << sid);
+      atomic_or(&cj->hydro.requires_sorts, 1 << sid);
+      ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
+      cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
+
+      /* Do we need to sort the cells? */
+      cell_activate_rt_sorts(ci, sid, s);
+      cell_activate_rt_sorts(cj, sid, s);
+    }
+  }
+}
+
 /**
  * @brief Un-skips all the hydro tasks associated with a given cell and checks
  * if the space needs to be rebuilt.
@@ -2746,18 +2925,25 @@ int cell_unskip_sinks_tasks(struct cell *c, struct scheduler *s) {
  *
  * @param c the #cell.
  * @param s the #scheduler.
+ * @param sub_cycle 1 if this is unskipping during an RT subcycle, 0 if normal
+ * unskip
  *
  * @return 1 If the space needs rebuilding. 0 otherwise.
  */
-int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
+int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s,
+                         const int sub_cycle) {
 
-  /* Note: we only get this far if engine_policy_rt is flagged. */
+  /* Do we have work here? */
+  if (c->hydro.count == 0) return 0;
 
   struct engine *e = s->space->e;
   const int nodeID = e->nodeID;
   int rebuild = 0; /* TODO: implement rebuild conditions? */
 
-  for (struct link *l = c->hydro.rt_gradient; l != NULL; l = l->next) {
+  /* Note: we only get this far if engine_policy_rt is flagged. */
+  if (!(e->policy & engine_policy_rt)) error("Unskipping RT tasks without RT");
+
+  for (struct link *l = c->rt.rt_gradient; l != NULL; l = l->next) {
 
     struct task *t = l->t;
     struct cell *ci = t->ci;
@@ -2769,32 +2955,36 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
     const int ci_nodeID = nodeID;
     const int cj_nodeID = nodeID;
 #endif
-    const int ci_active = cell_is_active_hydro(ci, e);
-    const int cj_active = (cj != NULL) && cell_is_active_hydro(cj, e);
+    const int ci_active = cell_is_rt_active(ci, e);
+    const int cj_active = (cj != NULL) && cell_is_rt_active(cj, e);
 
+    /* Only activate tasks that involve a local active cell. */
     if ((ci_active && ci_nodeID == nodeID) ||
         (cj_active && cj_nodeID == nodeID)) {
       scheduler_activate(s, t);
 
-      /* Activate hydro drift */
-      if (t->type == task_type_self) {
-        if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-      }
-
-      else if (t->type == task_type_pair || t->type == task_type_sub_pair) {
+      if (!sub_cycle) {
+        /* Activate sorts only during main/normal steps. */
+        if (t->type == task_type_pair) {
+          atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
+          atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
+          ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
+          cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
 
-        atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
-        atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
-        ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
-        cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
+          /* Check the sorts and activate them if needed. */
+          cell_activate_rt_sorts(ci, t->flags, s);
+          cell_activate_rt_sorts(cj, t->flags, s);
+        }
 
-        /* Activate the drift tasks. */
-        if (ci_nodeID == nodeID) cell_activate_drift_part(ci, s);
-        if (cj_nodeID == nodeID) cell_activate_drift_part(cj, s);
+        /* Store current values of dx_max and h_max. */
+        else if (t->type == task_type_sub_self) {
+          cell_activate_subcell_rt_tasks(ci, NULL, s, sub_cycle);
+        }
 
-        /* Check the sorts and activate them if needed. */
-        cell_activate_hydro_sorts(ci, t->flags, s);
-        cell_activate_hydro_sorts(cj, t->flags, s);
+        /* Store current values of dx_max and h_max. */
+        else if (t->type == task_type_sub_pair) {
+          cell_activate_subcell_rt_tasks(ci, cj, s, sub_cycle);
+        }
       }
     }
 
@@ -2805,13 +2995,20 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
 
       /* Activate the send/recv tasks. */
       if (ci_nodeID != nodeID) {
-
         /* If the local cell is active, receive data from the foreign cell. */
         if (cj_active) {
           scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rt_gradient);
+          if (sub_cycle) {
+            /* If we're in a sub-cycle, then there should be no sorts. But since
+             * hydro sorts won't be active then, the RT sorts would run. Make
+             * sure the cells are also marked to skip the RT sorts, otherwise
+             * the 'sorted' flags will be wrongly set after a recv rt_gradient.
+             * The recv tasks might also run on a higher level than the current
+             * cell, so walk all the way up. */
+            cell_set_skip_rt_sort_flag_up(ci);
+          }
 
-          /* We only need updates later on if the other cell is active as well
-           */
+          /* We only need updates later on if the other cell is active too */
           if (ci_active) {
             scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rt_transport);
           }
@@ -2823,10 +3020,6 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
           scheduler_activate_send(s, cj->mpi.send, task_subtype_rt_gradient,
                                   ci_nodeID);
 
-          /* Drift the cell which will be sent; note that not all sent
-             particles will be drifted, only those that are needed. */
-          cell_activate_drift_part(cj, s);
-
           if (cj_active) {
             scheduler_activate_send(s, cj->mpi.send, task_subtype_rt_transport,
                                     ci_nodeID);
@@ -2838,9 +3031,12 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
         /* If the local cell is active, receive data from the foreign cell. */
         if (ci_active) {
           scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rt_gradient);
+          if (sub_cycle) {
+            /* No RT sorts during sub-cycling */
+            cell_set_skip_rt_sort_flag_up(cj);
+          }
 
-          /* We only need updates later on if the other cell is active as well
-           */
+          /* We only need updates later on if the other cell is active too */
           if (cj_active) {
             scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rt_transport);
           }
@@ -2852,10 +3048,6 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
           scheduler_activate_send(s, ci->mpi.send, task_subtype_rt_gradient,
                                   cj_nodeID);
 
-          /* Drift the cell which will be sent; note that not all sent
-             particles will be drifted, only those that are needed. */
-          cell_activate_drift_part(ci, s);
-
           if (ci_active) {
             scheduler_activate_send(s, ci->mpi.send, task_subtype_rt_transport,
                                     cj_nodeID);
@@ -2866,9 +3058,7 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
     }
   }
 
-  for (struct link *l = c->hydro.rt_transport; l != NULL; l = l->next) {
-    /* I assume that all hydro related subcell unskipping/activation necessary
-     * here is being done in the hydro part of cell_unskip */
+  for (struct link *l = c->rt.rt_transport; l != NULL; l = l->next) {
 
     struct task *t = l->t;
     struct cell *ci = t->ci;
@@ -2881,8 +3071,8 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
     const int cj_nodeID = nodeID;
 #endif
 
-    const int ci_active = cell_is_active_hydro(ci, e);
-    const int cj_active = ((cj != NULL) && cell_is_active_hydro(cj, e));
+    const int ci_active = cell_is_rt_active(ci, e);
+    const int cj_active = ((cj != NULL) && cell_is_rt_active(cj, e));
 
     if ((ci_active && ci_nodeID == nodeID) ||
         (cj_active && cj_nodeID == nodeID)) {
@@ -2893,23 +3083,32 @@ int cell_unskip_rt_tasks(struct cell *c, struct scheduler *s) {
         /* Activate transport_out for each cell that is part of
          * a pair/sub_pair task as to not miss any dependencies */
         if (ci_nodeID == nodeID)
-          scheduler_activate(s, ci->hydro.super->hydro.rt_transport_out);
+          scheduler_activate(s, ci->hydro.super->rt.rt_transport_out);
         if (cj_nodeID == nodeID)
-          scheduler_activate(s, cj->hydro.super->hydro.rt_transport_out);
+          scheduler_activate(s, cj->hydro.super->rt.rt_transport_out);
       }
     }
   }
 
   /* Unskip all the other task types */
-
-  if (cell_is_active_hydro(c, e)) {
-    if (c->hydro.rt_in != NULL) scheduler_activate(s, c->hydro.rt_in);
-    if (c->hydro.rt_ghost1 != NULL) scheduler_activate(s, c->hydro.rt_ghost1);
-    if (c->hydro.rt_ghost2 != NULL) scheduler_activate(s, c->hydro.rt_ghost2);
-    if (c->hydro.rt_transport_out != NULL)
-      scheduler_activate(s, c->hydro.rt_transport_out);
-    if (c->hydro.rt_tchem != NULL) scheduler_activate(s, c->hydro.rt_tchem);
-    if (c->hydro.rt_out != NULL) scheduler_activate(s, c->hydro.rt_out);
+  if (cell_is_rt_active(c, e)) {
+    if (c->nodeID == nodeID) {
+      if (c->rt.rt_in != NULL) scheduler_activate(s, c->rt.rt_in);
+      if (c->rt.rt_ghost1 != NULL) scheduler_activate(s, c->rt.rt_ghost1);
+      if (c->rt.rt_ghost2 != NULL) scheduler_activate(s, c->rt.rt_ghost2);
+      if (c->rt.rt_transport_out != NULL)
+        scheduler_activate(s, c->rt.rt_transport_out);
+      if (c->rt.rt_tchem != NULL) scheduler_activate(s, c->rt.rt_tchem);
+      if (c->rt.rt_out != NULL) scheduler_activate(s, c->rt.rt_out);
+    }
+    /* The rt_advance_cell_time tasks also run on foreign cells */
+    if (c->super != NULL && c->super->rt.rt_advance_cell_time != NULL)
+      scheduler_activate(s, c->super->rt.rt_advance_cell_time);
+    /* The rt_collect_times tasks replace the timestep_collect tasks
+     * during sub-cycles, so we only activate it when sub-cycling. */
+    if (c->rt.rt_collect_times != NULL && sub_cycle)
+      scheduler_activate(s, c->rt.rt_collect_times);
   }
+
   return rebuild;
 }
diff --git a/src/collectgroup.c b/src/collectgroup.c
index 04d2432d16560ca32149444c0c79383f3b23b62a..f876ccb37f86322617756bba75c638a8909eba27 100644
--- a/src/collectgroup.c
+++ b/src/collectgroup.c
@@ -40,11 +40,13 @@ struct mpicollectgroup1 {
   long long updated, g_updated, s_updated, sink_updated, b_updated;
   long long inhibited, g_inhibited, s_inhibited, sink_inhibited, b_inhibited;
   integertime_t ti_hydro_end_min;
+  integertime_t ti_rt_end_min;
   integertime_t ti_gravity_end_min;
   integertime_t ti_stars_end_min;
   integertime_t ti_sinks_end_min;
   integertime_t ti_black_holes_end_min;
   integertime_t ti_hydro_beg_max;
+  integertime_t ti_rt_beg_max;
   integertime_t ti_gravity_beg_max;
   integertime_t ti_stars_beg_max;
   integertime_t ti_sinks_beg_max;
@@ -99,6 +101,8 @@ void collectgroup1_apply(const struct collectgroup1 *grp1, struct engine *e) {
 
   e->ti_hydro_end_min = grp1->ti_hydro_end_min;
   e->ti_hydro_beg_max = grp1->ti_hydro_beg_max;
+  e->ti_rt_end_min = grp1->ti_rt_end_min;
+  e->ti_rt_beg_max = grp1->ti_rt_beg_max;
   e->ti_gravity_end_min = grp1->ti_gravity_end_min;
   e->ti_gravity_beg_max = grp1->ti_gravity_beg_max;
   e->ti_stars_end_min = grp1->ti_stars_end_min;
@@ -204,7 +208,8 @@ void collectgroup1_init(
     size_t s_updated, size_t sink_updated, size_t b_updated, size_t inhibited,
     size_t g_inhibited, size_t s_inhibited, size_t sink_inhibited,
     size_t b_inhibited, integertime_t ti_hydro_end_min,
-    integertime_t ti_hydro_beg_max, integertime_t ti_gravity_end_min,
+    integertime_t ti_hydro_beg_max, integertime_t ti_rt_end_min,
+    integertime_t ti_rt_beg_max, integertime_t ti_gravity_end_min,
     integertime_t ti_gravity_beg_max, integertime_t ti_stars_end_min,
     integertime_t ti_stars_beg_max, integertime_t ti_sinks_end_min,
     integertime_t ti_sinks_beg_max, integertime_t ti_black_holes_end_min,
@@ -225,6 +230,8 @@ void collectgroup1_init(
   grp1->sink_inhibited = sink_inhibited;
   grp1->ti_hydro_end_min = ti_hydro_end_min;
   grp1->ti_hydro_beg_max = ti_hydro_beg_max;
+  grp1->ti_rt_end_min = ti_rt_end_min;
+  grp1->ti_rt_beg_max = ti_rt_beg_max;
   grp1->ti_gravity_end_min = ti_gravity_end_min;
   grp1->ti_gravity_beg_max = ti_gravity_beg_max;
   grp1->ti_stars_end_min = ti_stars_end_min;
@@ -271,11 +278,13 @@ void collectgroup1_reduce(struct collectgroup1 *grp1) {
   mpigrp11.sink_inhibited = grp1->sink_inhibited;
   mpigrp11.b_inhibited = grp1->b_inhibited;
   mpigrp11.ti_hydro_end_min = grp1->ti_hydro_end_min;
+  mpigrp11.ti_rt_end_min = grp1->ti_rt_end_min;
   mpigrp11.ti_gravity_end_min = grp1->ti_gravity_end_min;
   mpigrp11.ti_stars_end_min = grp1->ti_stars_end_min;
   mpigrp11.ti_sinks_end_min = grp1->ti_sinks_end_min;
   mpigrp11.ti_black_holes_end_min = grp1->ti_black_holes_end_min;
   mpigrp11.ti_hydro_beg_max = grp1->ti_hydro_beg_max;
+  mpigrp11.ti_rt_beg_max = grp1->ti_rt_beg_max;
   mpigrp11.ti_gravity_beg_max = grp1->ti_gravity_beg_max;
   mpigrp11.ti_stars_beg_max = grp1->ti_stars_beg_max;
   mpigrp11.ti_sinks_beg_max = grp1->ti_sinks_beg_max;
@@ -309,11 +318,13 @@ void collectgroup1_reduce(struct collectgroup1 *grp1) {
   grp1->sink_inhibited = mpigrp12.sink_inhibited;
   grp1->b_inhibited = mpigrp12.b_inhibited;
   grp1->ti_hydro_end_min = mpigrp12.ti_hydro_end_min;
+  grp1->ti_rt_end_min = mpigrp12.ti_rt_end_min;
   grp1->ti_gravity_end_min = mpigrp12.ti_gravity_end_min;
   grp1->ti_stars_end_min = mpigrp12.ti_stars_end_min;
   grp1->ti_sinks_end_min = mpigrp12.ti_sinks_end_min;
   grp1->ti_black_holes_end_min = mpigrp12.ti_black_holes_end_min;
   grp1->ti_hydro_beg_max = mpigrp12.ti_hydro_beg_max;
+  grp1->ti_rt_beg_max = mpigrp12.ti_rt_beg_max;
   grp1->ti_gravity_beg_max = mpigrp12.ti_gravity_beg_max;
   grp1->ti_stars_beg_max = mpigrp12.ti_stars_beg_max;
   grp1->ti_sinks_beg_max = mpigrp12.ti_sinks_beg_max;
@@ -362,6 +373,8 @@ static void doreduce1(struct mpicollectgroup1 *mpigrp11,
   /* Minimum end time. */
   mpigrp11->ti_hydro_end_min =
       min(mpigrp11->ti_hydro_end_min, mpigrp12->ti_hydro_end_min);
+  mpigrp11->ti_rt_end_min =
+      min(mpigrp11->ti_rt_end_min, mpigrp12->ti_rt_end_min);
   mpigrp11->ti_gravity_end_min =
       min(mpigrp11->ti_gravity_end_min, mpigrp12->ti_gravity_end_min);
   mpigrp11->ti_stars_end_min =
@@ -374,6 +387,8 @@ static void doreduce1(struct mpicollectgroup1 *mpigrp11,
   /* Maximum beg time. */
   mpigrp11->ti_hydro_beg_max =
       max(mpigrp11->ti_hydro_beg_max, mpigrp12->ti_hydro_beg_max);
+  mpigrp11->ti_rt_beg_max =
+      max(mpigrp11->ti_rt_beg_max, mpigrp12->ti_rt_beg_max);
   mpigrp11->ti_gravity_beg_max =
       max(mpigrp11->ti_gravity_beg_max, mpigrp12->ti_gravity_beg_max);
   mpigrp11->ti_stars_beg_max =
diff --git a/src/collectgroup.h b/src/collectgroup.h
index 6a4efaa976e02a20febfe580009151033a71b8b9..68815dab0f98035f5bfc4b3616e00da96fa124a4 100644
--- a/src/collectgroup.h
+++ b/src/collectgroup.h
@@ -46,6 +46,7 @@ struct collectgroup1 {
 
   /* Times for the time-step */
   integertime_t ti_hydro_end_min, ti_hydro_beg_max;
+  integertime_t ti_rt_end_min, ti_rt_beg_max;
   integertime_t ti_gravity_end_min, ti_gravity_beg_max;
   integertime_t ti_stars_end_min, ti_stars_beg_max;
   integertime_t ti_black_holes_end_min, ti_black_holes_beg_max;
@@ -83,7 +84,8 @@ void collectgroup1_init(
     size_t s_updated, size_t b_updated, size_t sink_updated, size_t inhibited,
     size_t g_inhibited, size_t s_inhibited, size_t sink_inhibited,
     size_t b_inhibited, integertime_t ti_hydro_end_min,
-    integertime_t ti_hydro_beg_max, integertime_t ti_gravity_end_min,
+    integertime_t ti_hydro_beg_max, integertime_t ti_rt_end_min,
+    integertime_t ti_rt_beg_max, integertime_t ti_gravity_end_min,
     integertime_t ti_gravity_beg_max, integertime_t ti_stars_end_min,
     integertime_t ti_stars_beg_max, integertime_t ti_sinks_end_min,
     integertime_t ti_sinks_beg_max, integertime_t ti_black_holes_end_min,
diff --git a/src/engine.c b/src/engine.c
index 59b35cc60db81369cf94f298be642e31fd24768d..63bda426c52d0f9393665ecb153d1e02bd1b3a37 100644
--- a/src/engine.c
+++ b/src/engine.c
@@ -1128,11 +1128,12 @@ int engine_estimate_nr_tasks(const struct engine *e) {
     /* gradient: 1 self + 13 pairs                   |   14
      * transport: 1 self + 13 pairs                  | + 14
      * implicits: in + out, transport_out            | +  3
-     * others: ghost1, ghost2, thermochemistry       | +  3
+     * others: ghost1, ghost2, tchem, cell advance   | +  4
+     * sort, collect_times                           | +  2
      * 2 extra space                                 | +  2 */
-    n1 += 36;
+    n1 += 39;
 #ifdef WITH_MPI
-    n1 += 4; /* TODO: check this */
+    n1 += 2;
 #endif
   }
 
@@ -1622,6 +1623,7 @@ void engine_skip_force_and_kick(struct engine *e) {
         t->type == task_type_bh_swallow_ghost3 || t->type == task_type_bh_in ||
         t->type == task_type_bh_out || t->type == task_type_rt_ghost1 ||
         t->type == task_type_rt_ghost2 || t->type == task_type_rt_tchem ||
+        t->type == task_type_rt_advance_cell_time ||
         t->type == task_type_neutrino_weight || t->type == task_type_csds ||
         t->subtype == task_subtype_force ||
         t->subtype == task_subtype_limiter ||
@@ -1770,6 +1772,89 @@ void engine_get_max_ids(struct engine *e) {
 #endif
 }
 
+/**
+ * @brief Run the radiative transfer sub-cycles outside the
+ * regular time-steps.
+ *
+ * @param e The #engine
+ **/
+void engine_run_rt_sub_cycles(struct engine *e) {
+
+  /* Do we have work to do? */
+  if (!(e->policy & engine_policy_rt)) return;
+  if (e->max_nr_rt_subcycles <= 1) return;
+
+  /* Get the subcycling step */
+  const integertime_t rt_step_size = e->ti_rt_end_min - e->ti_current;
+  if (rt_step_size == 0) {
+    /* When we arrive at the final step, the rt_step_size can be == 0 */
+    if (!engine_is_done(e)) error("Got rt_step_size = 0");
+    return;
+  }
+
+  /* At this point, the non-RT ti_end_min is up-to-date. Use that and
+   * the time of the previous regular step to get how many subcycles
+   * we need. */
+  const int nr_rt_cycles = (e->ti_end_min - e->ti_current) / rt_step_size;
+  /* You can't check here that the number of cycles is exactly the number
+   * you fixed it to be. E.g. stars or gravity may reduce the time step
+   * sizes for some main steps such that they coincide with the RT bins,
+   * yielding effectively no subcycles. (At least for low numbers.) */
+
+  /* Get some time variables for printouts. Don't update the ones in the
+   * engine like in the regular step, or the outputs in the regular steps
+   * will be wrong. */
+  /* think cosmology one day: needs adapting here */
+  if (e->policy & engine_policy_cosmology)
+    error("Can't run RT subcycling with cosmology yet");
+  const double dt_subcycle = rt_step_size * e->time_base;
+  double time = e->ti_current_subcycle * e->time_base + e->time_begin;
+
+  /* Collect and print info before it's gone */
+  engine_collect_end_of_sub_cycle(e);
+  if (e->nodeID == 0) {
+    printf(
+        "  %6d cycle   0 (during regular tasks) dt=%14e "
+        "min/max active bin=%2d/%2d rt_updates=%18lld\n",
+        e->step, dt_subcycle, e->min_active_bin_subcycle,
+        e->max_active_bin_subcycle, e->rt_updates);
+  }
+
+  /* Note: zeroth sub-cycle already happened during the regular tasks,
+   * so we need to do one less than that. */
+  for (int sub_cycle = 1; sub_cycle < nr_rt_cycles; ++sub_cycle) {
+
+    e->rt_updates = 0ll;
+    integertime_t ti_subcycle_old = e->ti_current_subcycle;
+    e->ti_current_subcycle = e->ti_current + sub_cycle * rt_step_size;
+    e->max_active_bin_subcycle = get_max_active_bin(e->ti_current_subcycle);
+    e->min_active_bin_subcycle =
+        get_min_active_bin(e->ti_current_subcycle, ti_subcycle_old);
+    /* think cosmology one day: needs adapting here */
+    if (e->policy & engine_policy_cosmology)
+      error("Can't run RT subcycling with cosmology yet");
+    time = e->ti_current_subcycle * e->time_base + e->time_begin;
+
+    /* Do the actual work now. */
+    engine_unskip_rt_sub_cycle(e);
+    engine_launch(e, "cycles");
+
+    /* Collect number of updates and print */
+    engine_collect_end_of_sub_cycle(e);
+
+    if (e->nodeID == 0) {
+      printf(
+          "  %6d cycle %3d time=%13.6e     dt=%14e "
+          "min/max active bin=%2d/%2d rt_updates=%18lld\n",
+          e->step, sub_cycle, time, dt_subcycle, e->min_active_bin_subcycle,
+          e->max_active_bin_subcycle, e->rt_updates);
+    }
+  }
+
+  /* Once we're done, clean up after ourselves */
+  e->rt_updates = 0ll;
+}
+
 /**
  * @brief Initialises the particles and set them in a state ready to move
  *forward in time.
@@ -1947,12 +2032,6 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
   engine_launch(e, "tasks");
   TIMER_TOC2(timer_runners);
 
-  /* Initialise additional RT data now that time bins are set */
-#ifdef SWIFT_RT_DEBUG_CHECKS
-  if (e->policy & engine_policy_rt)
-    space_convert_rt_quantities_after_zeroth_step(e->s, e->verbose);
-#endif
-
 #ifdef SWIFT_HYDRO_DENSITY_CHECKS
   /* Run the brute-force hydro calculation for some parts */
   if (e->policy & engine_policy_hydro)
@@ -2094,6 +2173,9 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
     }
   }
 
+  /* Run the RT sub-cycles now. */
+  engine_run_rt_sub_cycles(e);
+
   clocks_gettime(&time2);
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -2116,6 +2198,14 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs,
   e->force_checks_snapshot_flag = 0;
 #endif
 
+#ifdef SWIFT_RT_DEBUG_CHECKS
+  /* if we're running the debug RT scheme, do some checks after every step,
+   * and reset debugging flags now. */
+  if (e->policy & engine_policy_rt) {
+    rt_debugging_checks_end_of_step(e, e->verbose);
+  }
+#endif
+
   if (e->verbose) message("took %.3f %s.", e->wallclock_time, clocks_getunit());
 }
 
@@ -2208,6 +2298,12 @@ int engine_step(struct engine *e) {
   engine_current_step = e->step;
   e->step_props = engine_step_prop_none;
 
+  /* RT sub-cycling related time updates */
+  e->max_active_bin_subcycle = get_max_active_bin(e->ti_end_min);
+  e->min_active_bin_subcycle =
+      get_min_active_bin(e->ti_end_min, e->ti_current_subcycle);
+  e->ti_current_subcycle = e->ti_end_min;
+
   /* When restarting, move everyone to the current time. */
   if (e->restarting) engine_drift_all(e, /*drift_mpole=*/1);
 
@@ -2538,6 +2634,9 @@ int engine_step(struct engine *e) {
     error("Obtained a time-step of size 0");
 #endif
 
+  /* Run the RT sub-cycling now. */
+  engine_run_rt_sub_cycles(e);
+
 #ifdef WITH_CSDS
   if (e->policy & engine_policy_csds && e->verbose)
     message("The CSDS currently uses %f GB of storage",
@@ -2574,6 +2673,14 @@ int engine_step(struct engine *e) {
     rt_debugging_checks_end_of_step(e, e->verbose);
 #endif
 
+#ifdef SWIFT_RT_DEBUG_CHECKS
+  /* if we're running the debug RT scheme, do some checks after every step.
+   * Do this after the output so we can safely reset debugging flags now. */
+  if (e->policy & engine_policy_rt) {
+    rt_debugging_checks_end_of_step(e, e->verbose);
+  }
+#endif
+
   TIMER_TOC2(timer_step);
 
   clocks_gettime(&time2);
@@ -2953,6 +3060,9 @@ void engine_init(
   e->time_end = 0.;
   e->max_active_bin = num_time_bins;
   e->min_active_bin = 1;
+  e->ti_current_subcycle = 0;
+  e->max_active_bin_subcycle = num_time_bins;
+  e->min_active_bin_subcycle = 1;
   e->internal_units = internal_units;
   e->output_list_snapshots = NULL;
   e->a_first_snapshot =
@@ -3008,6 +3118,8 @@ void engine_init(
   e->ps_output_count = 0;
   e->dt_min = parser_get_param_double(params, "TimeIntegration:dt_min");
   e->dt_max = parser_get_param_double(params, "TimeIntegration:dt_max");
+  e->max_nr_rt_subcycles = parser_get_opt_param_int(
+      params, "TimeIntegration:max_nr_rt_subcycles", /*default=*/0);
   e->dt_max_RMS_displacement = FLT_MAX;
   e->max_RMS_displacement_factor = parser_get_opt_param_double(
       params, "TimeIntegration:max_dt_RMS_factor", 0.25);
diff --git a/src/engine.h b/src/engine.h
index 42c2791c66daaba6f1b9b3b71ca2513df4aaabd6..5fa8db8508b351276b446ee8ff0055636bd93f52 100644
--- a/src/engine.h
+++ b/src/engine.h
@@ -196,6 +196,14 @@ struct engine {
   /* The lowest active bin at this time */
   timebin_t min_active_bin;
 
+  /* RT sub-cycling counterparts for timestepping vars */
+  integertime_t ti_current_subcycle;
+  timebin_t max_active_bin_subcycle;
+  timebin_t min_active_bin_subcycle;
+
+  /* Maximal number of radiative transfer sub-cycles per hydro step */
+  int max_nr_rt_subcycles;
+
   /* Time step */
   double time_step;
 
@@ -212,6 +220,12 @@ struct engine {
   /* Maximal hydro ti_beg for the next time-step */
   integertime_t ti_hydro_beg_max;
 
+  /* Minimal rt ti_end for the next time-step */
+  integertime_t ti_rt_end_min;
+
+  /* Maximal rt ti_beg for the next time-step */
+  integertime_t ti_rt_beg_max;
+
   /* Minimal gravity ti_end for the next time-step */
   integertime_t ti_gravity_end_min;
 
@@ -255,7 +269,7 @@ struct engine {
   integertime_t ti_beg_max;
 
   /* Number of particles updated in the previous step */
-  long long updates, g_updates, s_updates, b_updates, sink_updates;
+  long long updates, g_updates, s_updates, b_updates, sink_updates, rt_updates;
 
   /* Number of updates since the last rebuild */
   long long updates_since_rebuild;
@@ -654,6 +668,7 @@ void engine_compute_next_los_time(struct engine *e);
 void engine_compute_next_ps_time(struct engine *e);
 void engine_recompute_displacement_constraint(struct engine *e);
 void engine_unskip(struct engine *e);
+void engine_unskip_rt_sub_cycle(struct engine *e);
 void engine_drift_all(struct engine *e, const int drift_mpoles);
 void engine_drift_top_multipoles(struct engine *e);
 void engine_reconstruct_multipoles(struct engine *e);
@@ -661,6 +676,7 @@ void engine_allocate_foreign_particles(struct engine *e, const int fof);
 void engine_print_stats(struct engine *e);
 void engine_io(struct engine *e);
 void engine_collect_end_of_step(struct engine *e, int apply);
+void engine_collect_end_of_sub_cycle(struct engine *e);
 void engine_dump_snapshot(struct engine *e);
 void engine_run_on_dump(struct engine *e);
 void engine_init_output_lists(struct engine *e, struct swift_params *params,
@@ -696,6 +712,7 @@ void engine_config(int restart, int fof, struct engine *e,
                    const char *restart_file, struct repartition *reparttype);
 void engine_launch(struct engine *e, const char *call);
 int engine_prepare(struct engine *e);
+void engine_run_rt_sub_cycles(struct engine *e);
 void engine_init_particles(struct engine *e, int flag_entropy_ICs,
                            int clean_h_values);
 int engine_step(struct engine *e);
diff --git a/src/engine_collect_end_of_step.c b/src/engine_collect_end_of_step.c
index 94cb2fd3d1d0115c2144600a4d4532c865d9e55d..edca57333c939646e7e04fd22bf2ff2798e544b3 100644
--- a/src/engine_collect_end_of_step.c
+++ b/src/engine_collect_end_of_step.c
@@ -39,6 +39,7 @@ struct end_of_step_data {
   size_t updated, g_updated, s_updated, sink_updated, b_updated;
   size_t inhibited, g_inhibited, s_inhibited, sink_inhibited, b_inhibited;
   integertime_t ti_hydro_end_min, ti_hydro_beg_max;
+  integertime_t ti_rt_end_min, ti_rt_beg_max;
   integertime_t ti_gravity_end_min, ti_gravity_beg_max;
   integertime_t ti_stars_end_min, ti_stars_beg_max;
   integertime_t ti_sinks_end_min, ti_sinks_beg_max;
@@ -75,6 +76,7 @@ void engine_collect_end_of_step_mapper(void *map_data, int num_elements,
   size_t updated = 0, g_updated = 0, s_updated = 0, sink_updated = 0,
          b_updated = 0;
   integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_beg_max = 0;
+  integertime_t ti_rt_end_min = max_nr_timesteps, ti_rt_beg_max = 0;
   integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_beg_max = 0;
   integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_beg_max = 0;
   integertime_t ti_sinks_end_min = max_nr_timesteps, ti_sinks_beg_max = 0;
@@ -98,6 +100,10 @@ void engine_collect_end_of_step_mapper(void *map_data, int num_elements,
         ti_hydro_end_min = min(ti_hydro_end_min, c->hydro.ti_end_min);
       ti_hydro_beg_max = max(ti_hydro_beg_max, c->hydro.ti_beg_max);
 
+      if (c->rt.ti_rt_end_min > e->ti_current)
+        ti_rt_end_min = min(c->rt.ti_rt_end_min, ti_rt_end_min);
+      ti_rt_beg_max = max(c->rt.ti_rt_beg_max, ti_rt_beg_max);
+
       if (c->grav.ti_end_min > e->ti_current)
         ti_gravity_end_min = min(ti_gravity_end_min, c->grav.ti_end_min);
       ti_gravity_beg_max = max(ti_gravity_beg_max, c->grav.ti_beg_max);
@@ -156,6 +162,10 @@ void engine_collect_end_of_step_mapper(void *map_data, int num_elements,
       data->ti_hydro_end_min = min(ti_hydro_end_min, data->ti_hydro_end_min);
     data->ti_hydro_beg_max = max(ti_hydro_beg_max, data->ti_hydro_beg_max);
 
+    if (ti_rt_end_min > e->ti_current)
+      data->ti_rt_end_min = min(ti_rt_end_min, data->ti_rt_end_min);
+    data->ti_rt_beg_max = max(ti_rt_beg_max, data->ti_rt_beg_max);
+
     if (ti_gravity_end_min > e->ti_current)
       data->ti_gravity_end_min =
           min(ti_gravity_end_min, data->ti_gravity_end_min);
@@ -205,6 +215,7 @@ void engine_collect_end_of_step(struct engine *e, int apply) {
   data.updated = 0, data.g_updated = 0, data.s_updated = 0, data.b_updated = 0;
   data.sink_updated = 0;
   data.ti_hydro_end_min = max_nr_timesteps, data.ti_hydro_beg_max = 0;
+  data.ti_rt_end_min = max_nr_timesteps, data.ti_rt_beg_max = 0;
   data.ti_gravity_end_min = max_nr_timesteps, data.ti_gravity_beg_max = 0;
   data.ti_stars_end_min = max_nr_timesteps, data.ti_stars_beg_max = 0;
   data.ti_sinks_end_min = max_nr_timesteps, data.ti_sinks_beg_max = 0;
@@ -249,9 +260,10 @@ void engine_collect_end_of_step(struct engine *e, int apply) {
       &e->collect_group1, data.updated, data.g_updated, data.s_updated,
       data.sink_updated, data.b_updated, data.inhibited, data.g_inhibited,
       data.s_inhibited, data.sink_inhibited, data.b_inhibited,
-      data.ti_hydro_end_min, data.ti_hydro_beg_max, data.ti_gravity_end_min,
-      data.ti_gravity_beg_max, data.ti_stars_end_min, data.ti_stars_beg_max,
-      data.ti_sinks_end_min, data.ti_sinks_beg_max, data.ti_black_holes_end_min,
+      data.ti_hydro_end_min, data.ti_hydro_beg_max, data.ti_rt_end_min,
+      data.ti_rt_beg_max, data.ti_gravity_end_min, data.ti_gravity_beg_max,
+      data.ti_stars_end_min, data.ti_stars_beg_max, data.ti_sinks_end_min,
+      data.ti_sinks_beg_max, data.ti_black_holes_end_min,
       data.ti_black_holes_beg_max, e->forcerebuild, e->s->tot_cells,
       e->sched.nr_tasks, (float)e->sched.nr_tasks / (float)e->s->tot_cells,
       data.sfh, data.runtime, data.flush_lightcone_maps, data.deadtime,
@@ -340,3 +352,70 @@ void engine_collect_end_of_step(struct engine *e, int apply) {
     message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
             clocks_getunit());
 }
+
+/**
+ * @brief Mapping function to collect the data from the end of the sub-cycle
+ *
+ * @param map_data The list of cells with tasks on this node.
+ * @param num_elements The number of elements in the list this thread will work
+ * on.
+ * @param extra_data The #engine.
+ */
+void engine_collect_end_of_sub_cycle_mapper(void *map_data, int num_elements,
+                                            void *extra_data) {
+
+  struct engine *e = (struct engine *)extra_data;
+  struct space *s = e->s;
+  int *local_cells = (int *)map_data;
+
+  /* Local collectible */
+  long long rt_updated = 0LL;
+
+  for (int ind = 0; ind < num_elements; ind++) {
+    struct cell *c = &s->cells_top[local_cells[ind]];
+
+    if (c->hydro.count > 0) {
+
+      /* Aggregate data */
+      rt_updated += c->rt.updated;
+
+      /* Collected, so clear for next time. */
+      c->rt.updated = 0;
+    }
+  }
+
+  /* write back to the global data. */
+  atomic_add(&e->rt_updates, rt_updated);
+}
+
+/**
+ * @brief Collects additional data at the end of a subcycle.
+ * This function does not collect any data relevant to the
+ * time-steps or time integration.
+ *
+ * @param e The #engine.
+ */
+void engine_collect_end_of_sub_cycle(struct engine *e) {
+
+  const ticks tic = getticks();
+  struct space *s = e->s;
+
+  /* Collect information from the local top-level cells */
+  threadpool_map(&e->threadpool, engine_collect_end_of_sub_cycle_mapper,
+                 s->local_cells_top, s->nr_local_cells, sizeof(int),
+                 threadpool_auto_chunk_size, e);
+
+  /* Aggregate collective data from the different nodes for this step. */
+#ifdef WITH_MPI
+  long long rt_updates_tot = 0ll;
+  int test = MPI_Reduce(&e->rt_updates, &rt_updates_tot, 1, MPI_LONG_LONG,
+                        MPI_SUM, 0, MPI_COMM_WORLD);
+  if (test != MPI_SUCCESS) error("MPI reduce failed");
+  /* Overwrite only on rank 0. */
+  if (e->nodeID == 0) e->rt_updates = rt_updates_tot;
+#endif
+
+  if (e->verbose)
+    message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
+            clocks_getunit());
+}
diff --git a/src/engine_config.c b/src/engine_config.c
index cbc3ecdf5f8ad9101924bf312b991d17ce6ce577..c0dacacfc2c720d9ef323f66af7af88add102bde 100644
--- a/src/engine_config.c
+++ b/src/engine_config.c
@@ -541,6 +541,30 @@ void engine_config(int restart, int fof, struct engine *e,
     if (e->policy & engine_policy_stars)
       if (e->nodeID == 0) stars_props_print(e->stars_properties);
 
+    /* Print information about the RT scheme */
+    if (e->policy & engine_policy_rt) {
+      rt_props_print(e->rt_props);
+      if (e->nodeID == 0) {
+        if (e->max_nr_rt_subcycles <= 1)
+          message("WARNING: running without RT sub-cycling.");
+        else {
+          /* Make sure max_nr_rt_subcycles is an acceptable power of 2 */
+          timebin_t power_subcycles = 0;
+          while ((e->max_nr_rt_subcycles > (1 << power_subcycles)) &&
+                 power_subcycles < num_time_bins)
+            ++power_subcycles;
+          if (power_subcycles == num_time_bins)
+            error("TimeIntegration:max_nr_rt_subcycles=%d too big",
+                  e->max_nr_rt_subcycles);
+          if ((1 << power_subcycles) > e->max_nr_rt_subcycles)
+            error("TimeIntegration:max_nr_rt_subcycles=%d not a power of 2",
+                  e->max_nr_rt_subcycles);
+          message("Running up to %d RT sub-cycles per hydro step.",
+                  e->max_nr_rt_subcycles);
+        }
+      }
+    }
+
     /* Check we have sensible time bounds */
     if (e->time_begin >= e->time_end)
       error(
diff --git a/src/engine_maketasks.c b/src/engine_maketasks.c
index 3c7eb42f2bef72b4a5a3b50bba5553ea19c3e068..0b72af995bca37089dbfd17b9e1220b9995cee06 100644
--- a/src/engine_maketasks.c
+++ b/src/engine_maketasks.c
@@ -130,17 +130,23 @@ void engine_addtasks_send_gravity(struct engine *e, struct cell *ci,
  * @param t_gradient The send_gradient #task, if already created.
  * @param t_prep1 The send_prep1 #task, if it has already been created.
  * @param t_limiter The send_limiter #task, if it has already been created.
+ * @param t_rt_gradient The send_rt_gradient #task, if it has already been
+ * created.
+ * @param t_rt_transport The send_rt_transport #task, if it has already been
  * @param with_feedback Are we running with stellar feedback?
  * @param with_limiter Are we running with the time-step limiter?
  * @param with_sync Are we running with time-step synchronization?
+ * @param with_rt Are we running with radiative transfer?
  */
 void engine_addtasks_send_hydro(struct engine *e, struct cell *ci,
                                 struct cell *cj, struct task *t_xv,
                                 struct task *t_rho, struct task *t_gradient,
                                 struct task *t_prep1, struct task *t_limiter,
                                 struct task *t_pack_limiter,
+                                struct task *t_rt_gradient,
+                                struct task *t_rt_transport,
                                 const int with_feedback, const int with_limiter,
-                                const int with_sync) {
+                                const int with_sync, const int with_rt) {
 
 #ifdef WITH_MPI
   struct link *l = NULL;
@@ -191,6 +197,17 @@ void engine_addtasks_send_hydro(struct engine *e, struct cell *ci,
       }
 #endif
 
+      if (with_rt) {
+        /* Add the RT sends */
+        t_rt_gradient =
+            scheduler_addtask(s, task_type_send, task_subtype_rt_gradient,
+                              ci->mpi.tag, 0, ci, cj);
+
+        t_rt_transport =
+            scheduler_addtask(s, task_type_send, task_subtype_rt_transport,
+                              ci->mpi.tag, 0, ci, cj);
+      }
+
 #ifdef EXTRA_HYDRO_LOOP
 
       scheduler_addunlock(s, t_gradient, ci->hydro.super->hydro.end_force);
@@ -235,7 +252,39 @@ void engine_addtasks_send_hydro(struct engine *e, struct cell *ci,
         scheduler_addunlock(s, t_prep1, ci->hydro.super->stars.prep2_ghost);
       }
 #endif
-    }
+
+      if (with_rt) {
+        /* Don't send the transport stuff before the gradient stuff */
+        scheduler_addunlock(s, t_rt_gradient, t_rt_transport);
+
+        /* The send_gradient task depends on the cell's ghost1 task. */
+        scheduler_addunlock(s, ci->hydro.super->rt.rt_ghost1, t_rt_gradient);
+
+        /* The send_transport task depends on the cell's ghost2 task. */
+        scheduler_addunlock(s, ci->hydro.super->rt.rt_ghost2, t_rt_transport);
+
+        /* Safety measure: collect dependencies and make sure data is sent
+         * before modifying it */
+        scheduler_addunlock(s, t_rt_gradient, ci->hydro.super->rt.rt_ghost2);
+
+        /* Safety measure: collect dependencies and make sure data is sent
+         * before modifying it */
+        scheduler_addunlock(s, t_rt_transport,
+                            ci->hydro.super->rt.rt_transport_out);
+
+        /* Drift before you send. Especially intended to cover inactive cells
+         * being sent. */
+        scheduler_addunlock(s, ci->hydro.super->hydro.drift, t_rt_gradient);
+        scheduler_addunlock(s, ci->hydro.super->hydro.drift, t_rt_transport);
+
+        /* Make sure the gradient sends don't run before the xv is finished.
+         * This can occur when a cell itself is inactive for both hydro and
+         * RT, but needs to be sent over for some other cell's pair task.
+         * The rt_gradient - xv dependency is special because when received,
+         * these two tasks will/may activate the sorts.*/
+        scheduler_addunlock(s, t_xv, t_rt_gradient);
+      }
+    } /* if t_xv == NULL */
 
     /* Add them to the local cell. */
     engine_addlink(e, &ci->mpi.send, t_xv);
@@ -250,6 +299,12 @@ void engine_addtasks_send_hydro(struct engine *e, struct cell *ci,
 #ifdef EXTRA_STAR_LOOPS
     if (with_feedback) engine_addlink(e, &ci->mpi.send, t_prep1);
 #endif
+
+    if (with_rt) {
+      /* Add them to the local cell. */
+      engine_addlink(e, &ci->mpi.send, t_rt_gradient);
+      engine_addlink(e, &ci->mpi.send, t_rt_transport);
+    }
   }
 
   /* Recurse? */
@@ -258,7 +313,8 @@ void engine_addtasks_send_hydro(struct engine *e, struct cell *ci,
       if (ci->progeny[k] != NULL)
         engine_addtasks_send_hydro(
             e, ci->progeny[k], cj, t_xv, t_rho, t_gradient, t_prep1, t_limiter,
-            t_pack_limiter, with_feedback, with_limiter, with_sync);
+            t_pack_limiter, t_rt_gradient, t_rt_transport, with_feedback,
+            with_limiter, with_sync, with_rt);
 
 #else
   error("SWIFT was not compiled with MPI support.");
@@ -482,87 +538,6 @@ void engine_addtasks_send_black_holes(struct engine *e, struct cell *ci,
 #endif
 }
 
-/**
- * @brief Add send tasks for the RT pairs to a hierarchy of cells.
- *
- * @param e The #engine.
- * @param ci The sending #cell.
- * @param cj Dummy cell containing the nodeID of the receiving node.
- * @param t_rt_gradient The send_rt_gradient #task, if it has already been
- * created.
- * @param t_rt_transport The send_rt_transport #task, if it has already been
- * created.
- */
-void engine_addtasks_send_rt(struct engine *e, struct cell *ci, struct cell *cj,
-                             struct task *t_rt_gradient,
-                             struct task *t_rt_transport) {
-
-#ifdef WITH_MPI
-  struct link *l = NULL;
-  struct scheduler *s = &e->sched;
-  const int nodeID = cj->nodeID;
-
-  /* Early abort (are we below the level where tasks are)? */
-  if (!cell_get_flag(ci, cell_flag_has_tasks)) return;
-
-  /* Check if any of the density tasks are for the target node. */
-  for (l = ci->hydro.density; l != NULL; l = l->next)
-    if (l->t->ci->nodeID == nodeID ||
-        (l->t->cj != NULL && l->t->cj->nodeID == nodeID))
-      break;
-
-  /* If so, attach send tasks. */
-  if (l != NULL) {
-
-    /* Create the tasks and their dependencies? */
-    if (t_rt_gradient == NULL) {
-
-      /* Make sure this cell is tagged. */
-      cell_ensure_tagged(ci);
-
-      t_rt_gradient = scheduler_addtask(
-          s, task_type_send, task_subtype_rt_gradient, ci->mpi.tag, 0, ci, cj);
-      t_rt_transport = scheduler_addtask(
-          s, task_type_send, task_subtype_rt_transport, ci->mpi.tag, 0, ci, cj);
-
-      /* The send_gradient task depends on the cell's ghost1 task. */
-      scheduler_addunlock(s, ci->hydro.super->hydro.rt_ghost1, t_rt_gradient);
-
-      /* The send_transport task depends on the cell's ghost2 task. */
-      scheduler_addunlock(s, ci->hydro.super->hydro.rt_ghost2, t_rt_transport);
-
-      /* Safety measure: collect dependencies and make sure data is sent before
-       * modifying it */
-      scheduler_addunlock(s, t_rt_gradient, ci->hydro.super->hydro.rt_ghost2);
-
-      /* Safety measure: collect dependencies and make sure data is sent before
-       * modifying it */
-      scheduler_addunlock(s, t_rt_transport,
-                          ci->hydro.super->hydro.rt_transport_out);
-
-      /* Drift before you send. Especially intended to cover inactive cells
-       * being sent. */
-      scheduler_addunlock(s, ci->hydro.super->hydro.drift, t_rt_gradient);
-      scheduler_addunlock(s, ci->hydro.super->hydro.drift, t_rt_transport);
-    }
-
-    /* Add them to the local cell. */
-    engine_addlink(e, &ci->mpi.send, t_rt_gradient);
-    engine_addlink(e, &ci->mpi.send, t_rt_transport);
-  }
-
-  /* Recurse? */
-  if (ci->split)
-    for (int k = 0; k < 8; k++)
-      if (ci->progeny[k] != NULL)
-        engine_addtasks_send_rt(e, ci->progeny[k], cj, t_rt_gradient,
-                                t_rt_transport);
-
-#else
-  error("SWIFT was not compiled with MPI support.");
-#endif
-}
-
 /**
  * @brief Add recv tasks for hydro pairs to a hierarchy of cells.
  *
@@ -575,18 +550,28 @@ void engine_addtasks_send_rt(struct engine *e, struct cell *ci, struct cell *cj,
  * @param t_limiter The recv_limiter #task, if it has already been created.
  * @param t_unpack_limiter The unpack_limiter #task, if it has already been
  * created.
+ * @param t_rt_gradient The recv_rt_gradient #task, if it has already been
+ * created.
+ * @param t_rt_transport The recv_rt_transport #task, if it has already been
+ * created.
+ * @param t_rt_advance_cell_time The rt_advance_cell_time #task, if it has
+ * already been created
+ * @param t_rt_sorts The rt_sort #task, if it has already been created.
  * @param tend The top-level time-step communication #task.
  * @param with_feedback Are we running with stellar feedback?
  * @param with_black_holes Are we running with black holes?
  * @param with_limiter Are we running with the time-step limiter?
  * @param with_sync Are we running with time-step synchronization?
+ * @param with_rt Are we running with radiative transfer?
  */
 void engine_addtasks_recv_hydro(
     struct engine *e, struct cell *c, struct task *t_xv, struct task *t_rho,
     struct task *t_gradient, struct task *t_prep1, struct task *t_limiter,
-    struct task *t_unpack_limiter, struct task *const tend,
-    const int with_feedback, const int with_black_holes, const int with_limiter,
-    const int with_sync) {
+    struct task *t_unpack_limiter, struct task *t_rt_gradient,
+    struct task *t_rt_transport, struct task *t_rt_advance_cell_time,
+    struct task *t_rt_sorts, struct task *const tend, const int with_feedback,
+    const int with_black_holes, const int with_limiter, const int with_sync,
+    const int with_rt) {
 
 #ifdef WITH_MPI
   struct scheduler *s = &e->sched;
@@ -630,6 +615,81 @@ void engine_addtasks_recv_hydro(
                                   c->mpi.tag, 0, c, NULL);
     }
 #endif
+
+    if (with_rt) {
+      /* Create the tasks. */
+      t_rt_gradient = scheduler_addtask(
+          s, task_type_recv, task_subtype_rt_gradient, c->mpi.tag, 0, c, NULL);
+      t_rt_transport = scheduler_addtask(
+          s, task_type_recv, task_subtype_rt_transport, c->mpi.tag, 0, c, NULL);
+      /* Also create the rt_advance_cell_time tasks for the foreign cells
+       * for the sub-cycling. */
+#ifdef SWIFT_RT_DEBUG_CHECKS
+      if (c->super == NULL)
+        error("trying to add rt_advance_cell_time above super level...");
+#endif
+      t_rt_advance_cell_time =
+          scheduler_addtask(s, task_type_rt_advance_cell_time,
+                            task_subtype_none, 0, 0, c->super, NULL);
+
+      c->super->rt.rt_advance_cell_time = t_rt_advance_cell_time;
+      /* Create the RT collect times task at the top level, if it hasn't
+       * already. */
+      if (c->top->rt.rt_collect_times == NULL)
+        c->top->rt.rt_collect_times =
+            scheduler_addtask(s, task_type_rt_collect_times, task_subtype_none,
+                              0, 0, c->top, NULL);
+      /* Don't run collect times before you run advance cell time */
+      scheduler_addunlock(s, t_rt_advance_cell_time,
+                          c->top->rt.rt_collect_times);
+
+      /* Make sure we sort after receiving RT data. The hydro sorts may or may
+       * not be active. Blocking them with dependencies deadlocks with MPI. So
+       * add a new sort task instead, which will just do nothing if the cell is
+       * already sorted. */
+      t_rt_sorts = scheduler_addtask(s, task_type_rt_sort, task_subtype_none, 0,
+                                     0, c, NULL);
+      c->rt.rt_sorts = t_rt_sorts;
+      if (c->hydro.sorts != NULL) {
+        /* Copy task flags. While these should always be empty for sorts, better
+         * be safe than spend hours looking for this. */
+        t_rt_sorts->flags = c->hydro.sorts->flags;
+        /* Make sure the normal hydro sorts run before the RT sorts run. */
+        scheduler_addunlock(s, c->hydro.sorts, t_rt_sorts);
+        /* Don't run gradients on unsorted cells. */
+        scheduler_addunlock(s, c->hydro.sorts, t_rt_gradient);
+      }
+
+      /* Make sure the second receive doesn't get enqueued before the first one
+       * is done */
+      scheduler_addunlock(s, t_rt_gradient, t_rt_sorts);
+      scheduler_addunlock(s, t_rt_gradient, t_rt_transport);
+      /* Avoid situation where we receive while the sort hasn't finished yet. */
+      scheduler_addunlock(s, t_rt_sorts, t_rt_transport);
+      /* If one or both recv tasks are active, make sure the
+       * rt_advance_cell_time tasks doesn't run before them */
+      scheduler_addunlock(s, t_rt_gradient, t_rt_advance_cell_time);
+      scheduler_addunlock(s, t_rt_transport, t_rt_advance_cell_time);
+      /* Make sure the gradient recv don't run before the xv is finished.
+       * This can occur when a cell itself is inactive for both hydro and
+       * RT, but needs to be sent over for some other cell's pair task.
+       * For active cells, you must make sure that t_rho and t_gradient have
+       * been received first. As there is no guarantee which message will
+       * arrive first, you might overwrite data otherwise. */
+
+      scheduler_addunlock(s, t_xv, t_rt_gradient);
+      scheduler_addunlock(s, t_rho, t_rt_gradient);
+#ifdef EXTRA_HYDRO_LOOP
+      scheduler_addunlock(s, t_gradient, t_rt_gradient);
+#endif
+
+      /* In normal steps, tend mustn't run before rt_advance_cell_time or the
+       * cell's ti_rt_end_min will be updated wrongly. In sub-cycles, we don't
+       * have the tend tasks, so there's no worry about that. (Them missing is
+       * the reason we need the rt_advanced_cell_time to complete the sub-cycles
+       * in the first place) */
+      scheduler_addunlock(s, t_rt_advance_cell_time, tend);
+    }
   }
 
   if (t_xv != NULL) {
@@ -709,16 +769,43 @@ void engine_addtasks_recv_hydro(
         scheduler_addunlock(s, t_rho, l->t);
       }
     }
+
+    if (with_rt) {
+      engine_addlink(e, &c->mpi.recv, t_rt_gradient);
+      engine_addlink(e, &c->mpi.recv, t_rt_transport);
+
+      for (struct link *l = c->rt.rt_gradient; l != NULL; l = l->next) {
+        /* RT gradient tasks mustn't run before we receive necessary data */
+        scheduler_addunlock(s, t_rt_gradient, l->t);
+        /* Don't run gradient tasks without sorting */
+        scheduler_addunlock(s, t_rt_sorts, l->t);
+        /* Don't update local particles before gradient tasks are finished */
+        scheduler_addunlock(s, l->t, t_rt_transport);
+      }
+
+      for (struct link *l = c->rt.rt_transport; l != NULL; l = l->next) {
+        /* RT transport tasks (iact, not comm tasks!!) mustn't run before we
+         * receive necessary data */
+        scheduler_addunlock(s, t_rt_transport, l->t);
+        /* add dependency for the timestep communication tasks. In cases where
+         * RT is inactive, rt_advance_cell_time won't run, so we need to make
+         * sure we don't receive data before we're done with all the work. */
+        scheduler_addunlock(s, l->t, tend);
+        /* advance cell time mustn't run before transport is done */
+        scheduler_addunlock(s, l->t, t_rt_advance_cell_time);
+      }
+    }
   }
 
   /* Recurse? */
   if (c->split)
     for (int k = 0; k < 8; k++)
       if (c->progeny[k] != NULL)
-        engine_addtasks_recv_hydro(e, c->progeny[k], t_xv, t_rho, t_gradient,
-                                   t_prep1, t_limiter, t_unpack_limiter, tend,
-                                   with_feedback, with_black_holes,
-                                   with_limiter, with_sync);
+        engine_addtasks_recv_hydro(
+            e, c->progeny[k], t_xv, t_rho, t_gradient, t_prep1, t_limiter,
+            t_unpack_limiter, t_rt_gradient, t_rt_transport,
+            t_rt_advance_cell_time, t_rt_sorts, tend, with_feedback,
+            with_black_holes, with_limiter, with_sync, with_rt);
 
 #else
   error("SWIFT was not compiled with MPI support.");
@@ -1003,78 +1090,6 @@ void engine_addtasks_recv_gravity(struct engine *e, struct cell *c,
 #endif
 }
 
-/**
- * @brief Add recv tasks for RT pairs to a hierarchy of cells.
- *
- * @param e The #engine.
- * @param c The foreign #cell.
- * @param t_rt_gradient The recv_rt_gradient #task, if it has already been
- * created.
- * @param t_rt_transport The recv_rt_transport #task, if it has already been
- * created.
- * @param tend The top-level time-step communication #task.
- */
-void engine_addtasks_recv_rt(struct engine *e, struct cell *c,
-                             struct task *t_rt_gradient,
-                             struct task *t_rt_transport, struct task *tend) {
-
-#ifdef WITH_MPI
-  struct scheduler *s = &e->sched;
-
-  /* Early abort (are we below the level where tasks are)? */
-  if (!cell_get_flag(c, cell_flag_has_tasks)) return;
-
-  /* Have we reached a level where there are any hydro tasks ? */
-  if (t_rt_gradient == NULL && c->hydro.density != NULL) {
-
-#ifdef SWIFT_DEBUG_CHECKS
-    /* Make sure this cell has a valid tag. */
-    if (c->mpi.tag < 0) error("Trying to receive from untagged cell.");
-#endif /* SWIFT_DEBUG_CHECKS */
-
-    /* Create the tasks. */
-    t_rt_gradient = scheduler_addtask(
-        s, task_type_recv, task_subtype_rt_gradient, c->mpi.tag, 0, c, NULL);
-    t_rt_transport = scheduler_addtask(
-        s, task_type_recv, task_subtype_rt_transport, c->mpi.tag, 0, c, NULL);
-
-    /* Make sure the second receive doens't get enqueued before the first one is
-     * done */
-    scheduler_addunlock(s, t_rt_gradient, t_rt_transport);
-  }
-
-  if (t_rt_gradient != NULL) {
-    engine_addlink(e, &c->mpi.recv, t_rt_gradient);
-    engine_addlink(e, &c->mpi.recv, t_rt_transport);
-
-    if (c->hydro.sorts != NULL) {
-      scheduler_addunlock(s, c->hydro.sorts, t_rt_transport);
-    }
-
-    for (struct link *l = c->hydro.rt_gradient; l != NULL; l = l->next) {
-      /* RT gradient tasks mustn't run before we receive necessary data */
-      scheduler_addunlock(s, t_rt_gradient, l->t);
-      scheduler_addunlock(s, l->t, t_rt_transport);
-    }
-
-    for (struct link *l = c->hydro.rt_transport; l != NULL; l = l->next) {
-      /* RT transport tasks mustn't run before we receive necessary data */
-      scheduler_addunlock(s, t_rt_transport, l->t);
-      /* add dependency for the timestep communication tasks */
-      scheduler_addunlock(s, l->t, tend);
-    }
-  }
-  /* Recurse? */
-  if (c->split)
-    for (int k = 0; k < 8; k++)
-      if (c->progeny[k] != NULL)
-        engine_addtasks_recv_rt(e, c->progeny[k], t_rt_gradient, t_rt_transport,
-                                tend);
-#else
-  error("SWIFT was not compiled with MPI support.");
-#endif
-}
-
 /**
  * @brief Generate the hydro hierarchical tasks for a hierarchy of cells -
  * i.e. all the O(Npart) tasks -- timestep version
@@ -1097,6 +1112,7 @@ void engine_make_hierarchical_tasks_common(struct engine *e, struct cell *c) {
   const int with_timestep_limiter =
       (e->policy & engine_policy_timestep_limiter);
   const int with_timestep_sync = (e->policy & engine_policy_timestep_sync);
+  const int with_rt = (e->policy & engine_policy_rt);
 #ifdef WITH_CSDS
   const int with_csds = e->policy & engine_policy_csds;
 #endif
@@ -1127,6 +1143,11 @@ void engine_make_hierarchical_tasks_common(struct engine *e, struct cell *c) {
       c->sinks.sink_formation = scheduler_addtask(
           s, task_type_sink_formation, task_subtype_none, 0, 0, c, NULL);
     }
+
+    if (with_rt) {
+      c->rt.rt_collect_times = scheduler_addtask(
+          s, task_type_rt_collect_times, task_subtype_none, 0, 0, c, NULL);
+    }
   }
 
   /* Are we in a super-cell ? */
@@ -1623,48 +1644,70 @@ void engine_make_hierarchical_tasks_hydro(struct engine *e, struct cell *c,
       /* Radiative Transfer */
       if (with_rt) {
         /* RT ghost in task */
-        c->hydro.rt_in =
+        c->rt.rt_in =
             scheduler_addtask(s, task_type_rt_in, task_subtype_none, 0,
                               /* implicit= */ 1, c, NULL);
-        scheduler_addunlock(s, c->super->kick2, c->hydro.rt_in);
+        scheduler_addunlock(s, c->super->kick2, c->rt.rt_in);
         /* Star formation */
         if (c->top->hydro.count > 0 && with_star_formation)
-          scheduler_addunlock(s, c->top->hydro.star_formation, c->hydro.rt_in);
+          scheduler_addunlock(s, c->top->hydro.star_formation, c->rt.rt_in);
         /* Star formation from sinks */
         if (with_star_formation_sink &&
             (c->top->hydro.count > 0 || c->top->sinks.count > 0))
           scheduler_addunlock(s, c->top->sinks.star_formation_sink,
-                              c->hydro.rt_in);
+                              c->rt.rt_in);
         if (with_feedback)
-          scheduler_addunlock(s, c->stars.stars_out, c->hydro.rt_in);
+          scheduler_addunlock(s, c->stars.stars_out, c->rt.rt_in);
         /* TODO: check/add dependencies from Loic's new sink SF tasks */
 
         /* RT ghost out task */
-        c->hydro.rt_out =
+        c->rt.rt_out =
             scheduler_addtask(s, task_type_rt_out, task_subtype_none, 0,
                               /* implicit= */ 1, c, NULL);
-        scheduler_addunlock(s, c->hydro.rt_out, c->super->timestep);
+        scheduler_addunlock(s, c->rt.rt_out, c->super->timestep);
+
+        /* In cases where nothing but RT is active, don't allow the timestep
+         * collect to run before we've finished */
+        scheduler_addunlock(s, c->rt.rt_out, c->super->timestep_collect);
 
         /* non-implicit ghost 1 */
-        c->hydro.rt_ghost1 = scheduler_addtask(
-            s, task_type_rt_ghost1, task_subtype_none, 0, 0, c, NULL);
-        scheduler_addunlock(s, c->hydro.rt_in, c->hydro.rt_ghost1);
+        c->rt.rt_ghost1 = scheduler_addtask(s, task_type_rt_ghost1,
+                                            task_subtype_none, 0, 0, c, NULL);
+        scheduler_addunlock(s, c->rt.rt_in, c->rt.rt_ghost1);
 
         /* non-implicit ghost 2 */
-        c->hydro.rt_ghost2 = scheduler_addtask(
-            s, task_type_rt_ghost2, task_subtype_none, 0, 0, c, NULL);
+        c->rt.rt_ghost2 = scheduler_addtask(s, task_type_rt_ghost2,
+                                            task_subtype_none, 0, 0, c, NULL);
 
         /* implicit transport out */
-        c->hydro.rt_transport_out =
+        c->rt.rt_transport_out =
             scheduler_addtask(s, task_type_rt_transport_out, task_subtype_none,
                               0, /*implicit= */ 1, c, NULL);
 
-        /* non-implicit ghost 2 */
-        c->hydro.rt_tchem = scheduler_addtask(s, task_type_rt_tchem,
-                                              task_subtype_none, 0, 0, c, NULL);
+        /* thermochemistry */
+        c->rt.rt_tchem = scheduler_addtask(s, task_type_rt_tchem,
+                                           task_subtype_none, 0, 0, c, NULL);
+
+        /* Advance cell time for subcycling */
+        /* We need to make sure that rt_advance_cell_time is at the same level
+         * as the timestep task, not below. Otherwise, the updated cell times
+         * won't propagate up the hierarchy enough, and the cell_is_rt_active
+         * will return bogus results. Note that c->super is not necessarily
+         * c->hydro.super in general. */
+        /* Create the task only once ! */
+        if (c->super->rt.rt_advance_cell_time == NULL) {
+          c->super->rt.rt_advance_cell_time =
+              scheduler_addtask(s, task_type_rt_advance_cell_time,
+                                task_subtype_none, 0, 0, c->super, NULL);
+          /* Don't run the rt_collect_times before the rt_advance_cell_time */
+          scheduler_addunlock(s, c->super->rt.rt_advance_cell_time,
+                              c->top->rt.rt_collect_times);
+        }
 
-        scheduler_addunlock(s, c->hydro.rt_transport_out, c->hydro.rt_tchem);
-        scheduler_addunlock(s, c->hydro.rt_tchem, c->hydro.rt_out);
+        scheduler_addunlock(s, c->rt.rt_transport_out, c->rt.rt_tchem);
+        scheduler_addunlock(s, c->rt.rt_tchem,
+                            c->super->rt.rt_advance_cell_time);
+        scheduler_addunlock(s, c->super->rt.rt_advance_cell_time, c->rt.rt_out);
       }
 
       /* Subgrid tasks: black hole feedback */
@@ -2455,8 +2498,8 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         engine_addlink(e, &ci->black_holes.feedback, t_bh_feedback);
       }
       if (with_rt) {
-        engine_addlink(e, &ci->hydro.rt_gradient, t_rt_gradient);
-        engine_addlink(e, &ci->hydro.rt_transport, t_rt_transport);
+        engine_addlink(e, &ci->rt.rt_gradient, t_rt_gradient);
+        engine_addlink(e, &ci->rt.rt_transport, t_rt_transport);
       }
 
 #ifdef EXTRA_HYDRO_LOOP
@@ -2600,14 +2643,14 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
 
       if (with_rt) {
         scheduler_addunlock(sched, ci->hydro.super->hydro.drift, t_rt_gradient);
-        scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost1,
+        scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost1,
                             t_rt_gradient);
         scheduler_addunlock(sched, t_rt_gradient,
-                            ci->hydro.super->hydro.rt_ghost2);
-        scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost2,
+                            ci->hydro.super->rt.rt_ghost2);
+        scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost2,
                             t_rt_transport);
         scheduler_addunlock(sched, t_rt_transport,
-                            ci->hydro.super->hydro.rt_transport_out);
+                            ci->hydro.super->rt.rt_transport_out);
       }
     }
 
@@ -2734,10 +2777,10 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         engine_addlink(e, &cj->black_holes.feedback, t_bh_feedback);
       }
       if (with_rt) {
-        engine_addlink(e, &ci->hydro.rt_gradient, t_rt_gradient);
-        engine_addlink(e, &cj->hydro.rt_gradient, t_rt_gradient);
-        engine_addlink(e, &ci->hydro.rt_transport, t_rt_transport);
-        engine_addlink(e, &cj->hydro.rt_transport, t_rt_transport);
+        engine_addlink(e, &ci->rt.rt_gradient, t_rt_gradient);
+        engine_addlink(e, &cj->rt.rt_gradient, t_rt_gradient);
+        engine_addlink(e, &ci->rt.rt_transport, t_rt_transport);
+        engine_addlink(e, &cj->rt.rt_transport, t_rt_transport);
       }
 
 #ifdef EXTRA_HYDRO_LOOP
@@ -2924,14 +2967,14 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         if (with_rt) {
           scheduler_addunlock(sched, ci->hydro.super->hydro.drift,
                               t_rt_gradient);
-          scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost1,
+          scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost1,
                               t_rt_gradient);
           scheduler_addunlock(sched, t_rt_gradient,
-                              ci->hydro.super->hydro.rt_ghost2);
-          scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost2,
+                              ci->hydro.super->rt.rt_ghost2);
+          scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost2,
                               t_rt_transport);
           scheduler_addunlock(sched, t_rt_transport,
-                              ci->hydro.super->hydro.rt_transport_out);
+                              ci->hydro.super->rt.rt_transport_out);
         }
 
       } else /*(ci->nodeID != nodeID) */ {
@@ -3068,14 +3111,14 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
           if (with_rt) {
             scheduler_addunlock(sched, cj->hydro.super->hydro.drift,
                                 t_rt_gradient);
-            scheduler_addunlock(sched, cj->hydro.super->hydro.rt_ghost1,
+            scheduler_addunlock(sched, cj->hydro.super->rt.rt_ghost1,
                                 t_rt_gradient);
             scheduler_addunlock(sched, t_rt_gradient,
-                                cj->hydro.super->hydro.rt_ghost2);
-            scheduler_addunlock(sched, cj->hydro.super->hydro.rt_ghost2,
+                                cj->hydro.super->rt.rt_ghost2);
+            scheduler_addunlock(sched, cj->hydro.super->rt.rt_ghost2,
                                 t_rt_transport);
             scheduler_addunlock(sched, t_rt_transport,
-                                cj->hydro.super->hydro.rt_transport_out);
+                                cj->hydro.super->rt.rt_transport_out);
           }
 
           if (with_timestep_limiter) {
@@ -3227,8 +3270,8 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         engine_addlink(e, &ci->black_holes.feedback, t_bh_feedback);
       }
       if (with_rt) {
-        engine_addlink(e, &ci->hydro.rt_gradient, t_rt_gradient);
-        engine_addlink(e, &ci->hydro.rt_transport, t_rt_transport);
+        engine_addlink(e, &ci->rt.rt_gradient, t_rt_gradient);
+        engine_addlink(e, &ci->rt.rt_transport, t_rt_transport);
       }
 
 #ifdef EXTRA_HYDRO_LOOP
@@ -3378,14 +3421,14 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
       if (with_rt) {
         scheduler_addunlock(sched, ci->hydro.super->hydro.drift, t_rt_gradient);
         scheduler_addunlock(sched, ci->hydro.super->hydro.sorts, t_rt_gradient);
-        scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost1,
+        scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost1,
                             t_rt_gradient);
         scheduler_addunlock(sched, t_rt_gradient,
-                            ci->hydro.super->hydro.rt_ghost2);
-        scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost2,
+                            ci->hydro.super->rt.rt_ghost2);
+        scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost2,
                             t_rt_transport);
         scheduler_addunlock(sched, t_rt_transport,
-                            ci->hydro.super->hydro.rt_transport_out);
+                            ci->hydro.super->rt.rt_transport_out);
       }
     }
 
@@ -3523,10 +3566,10 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         engine_addlink(e, &cj->black_holes.feedback, t_bh_feedback);
       }
       if (with_rt) {
-        engine_addlink(e, &ci->hydro.rt_gradient, t_rt_gradient);
-        engine_addlink(e, &cj->hydro.rt_gradient, t_rt_gradient);
-        engine_addlink(e, &ci->hydro.rt_transport, t_rt_transport);
-        engine_addlink(e, &cj->hydro.rt_transport, t_rt_transport);
+        engine_addlink(e, &ci->rt.rt_gradient, t_rt_gradient);
+        engine_addlink(e, &cj->rt.rt_gradient, t_rt_gradient);
+        engine_addlink(e, &ci->rt.rt_transport, t_rt_transport);
+        engine_addlink(e, &cj->rt.rt_transport, t_rt_transport);
       }
 
 #ifdef EXTRA_HYDRO_LOOP
@@ -3712,14 +3755,14 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
         if (with_rt) {
           scheduler_addunlock(sched, ci->hydro.super->hydro.drift,
                               t_rt_gradient);
-          scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost1,
+          scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost1,
                               t_rt_gradient);
           scheduler_addunlock(sched, t_rt_gradient,
-                              ci->hydro.super->hydro.rt_ghost2);
-          scheduler_addunlock(sched, ci->hydro.super->hydro.rt_ghost2,
+                              ci->hydro.super->rt.rt_ghost2);
+          scheduler_addunlock(sched, ci->hydro.super->rt.rt_ghost2,
                               t_rt_transport);
           scheduler_addunlock(sched, t_rt_transport,
-                              ci->hydro.super->hydro.rt_transport_out);
+                              ci->hydro.super->rt.rt_transport_out);
         }
       } else /* ci->nodeID != nodeID */ {
 
@@ -3854,14 +3897,14 @@ void engine_make_extra_hydroloop_tasks_mapper(void *map_data, int num_elements,
           if (with_rt) {
             scheduler_addunlock(sched, cj->hydro.super->hydro.drift,
                                 t_rt_gradient);
-            scheduler_addunlock(sched, cj->hydro.super->hydro.rt_ghost1,
+            scheduler_addunlock(sched, cj->hydro.super->rt.rt_ghost1,
                                 t_rt_gradient);
             scheduler_addunlock(sched, t_rt_gradient,
-                                cj->hydro.super->hydro.rt_ghost2);
-            scheduler_addunlock(sched, cj->hydro.super->hydro.rt_ghost2,
+                                cj->hydro.super->rt.rt_ghost2);
+            scheduler_addunlock(sched, cj->hydro.super->rt.rt_ghost2,
                                 t_rt_transport);
             scheduler_addunlock(sched, t_rt_transport,
-                                cj->hydro.super->hydro.rt_transport_out);
+                                cj->hydro.super->rt.rt_transport_out);
           }
 
           if (with_timestep_limiter) {
@@ -4055,6 +4098,7 @@ void engine_addtasks_send_mapper(void *map_data, int num_elements,
   const int with_limiter = (e->policy & engine_policy_timestep_limiter);
   const int with_feedback = (e->policy & engine_policy_feedback);
   const int with_sync = (e->policy & engine_policy_timestep_sync);
+  const int with_rt = (e->policy & engine_policy_rt);
   struct cell_type_pair *cell_type_pairs = (struct cell_type_pair *)map_data;
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -4069,12 +4113,29 @@ void engine_addtasks_send_mapper(void *map_data, int num_elements,
     const int type = cell_type_pairs[k].type;
 
 #ifdef WITH_MPI
+
     if (!cell_is_empty(ci)) {
       /* Add the timestep exchange task */
       struct task *tend = scheduler_addtask(
           &e->sched, task_type_send, task_subtype_tend, ci->mpi.tag, 0, ci, cj);
       scheduler_addunlock(&e->sched, ci->timestep_collect, tend);
       engine_addlink(e, &ci->mpi.send, tend);
+
+      if (with_rt && (type & proxy_cell_type_hydro)) {
+
+        /* If we're running with RT subcycling, we need to ensure that nothing
+         * is sent before the advance cell time task has finished. This may
+         * overwrite the correct cell times, particularly so when we're sending
+         * over data for non-RT tasks, e.g. for gravity pair tasks. */
+        if (ci->super->rt.rt_advance_cell_time != NULL) {
+          scheduler_addunlock(&e->sched, ci->super->rt.rt_advance_cell_time,
+                              tend);
+#ifdef SWIFT_RT_DEBUG_CHECKS
+        } else {
+          error("Got local super cell without rt_advance_cell_time task");
+#endif
+        }
+      }
     }
 #endif
 
@@ -4085,7 +4146,9 @@ void engine_addtasks_send_mapper(void *map_data, int num_elements,
                                  /*t_rho=*/NULL, /*t_gradient=*/NULL,
                                  /*t_prep1=*/NULL,
                                  /*t_limiter=*/NULL, /*t_pack_limiter=*/NULL,
-                                 with_feedback, with_limiter, with_sync);
+                                 /*t_rt_gradient=*/NULL,
+                                 /*t_rt_transport=*/NULL, with_feedback,
+                                 with_limiter, with_sync, with_rt);
 
     /* Add the send tasks for the cells in the proxy that have a stars
      * connection. */
@@ -4108,10 +4171,6 @@ void engine_addtasks_send_mapper(void *map_data, int num_elements,
     if ((e->policy & engine_policy_self_gravity) &&
         (type & proxy_cell_type_gravity))
       engine_addtasks_send_gravity(e, ci, cj, /*t_grav=*/NULL);
-
-    if ((e->policy & engine_policy_rt) && (type & proxy_cell_type_hydro))
-      engine_addtasks_send_rt(e, ci, cj, /*t_rt_gradient=*/NULL,
-                              /*t_rt_transport=*/NULL);
   }
 }
 
@@ -4124,6 +4183,7 @@ void engine_addtasks_recv_mapper(void *map_data, int num_elements,
   const int with_feedback = (e->policy & engine_policy_feedback);
   const int with_black_holes = (e->policy & engine_policy_black_holes);
   const int with_sync = (e->policy & engine_policy_timestep_sync);
+  const int with_rt = (e->policy & engine_policy_rt);
   struct cell_type_pair *cell_type_pairs = (struct cell_type_pair *)map_data;
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -4148,13 +4208,14 @@ void engine_addtasks_recv_mapper(void *map_data, int num_elements,
 
     /* Add the recv tasks for the cells in the proxy that have a hydro
      * connection. */
-    if ((e->policy & engine_policy_hydro) && (type & proxy_cell_type_hydro))
-      engine_addtasks_recv_hydro(e, ci, /*t_xv=*/NULL, /*t_rho=*/NULL,
-                                 /*t_gradient=*/NULL,
-                                 /*t_prep1=*/NULL,
-                                 /*t_limiter=*/NULL, /*t_unpack_limiter=*/NULL,
-                                 tend, with_feedback, with_black_holes,
-                                 with_limiter, with_sync);
+    if ((e->policy & engine_policy_hydro) && (type & proxy_cell_type_hydro)) {
+      engine_addtasks_recv_hydro(
+          e, ci, /*t_xv=*/NULL, /*t_rho=*/NULL, /*t_gradient=*/NULL,
+          /*t_prep1=*/NULL, /*t_limiter=*/NULL, /*t_unpack_limiter=*/NULL,
+          /*t_rt_gradient=*/NULL, /*t_rt_transport=*/NULL,
+          /*t_rt_advance_cell_time=*/NULL, /*t_rt_sorts=*/NULL, tend,
+          with_feedback, with_black_holes, with_limiter, with_sync, with_rt);
+    }
 
     /* Add the recv tasks for the cells in the proxy that have a stars
      * connection. */
@@ -4177,12 +4238,6 @@ void engine_addtasks_recv_mapper(void *map_data, int num_elements,
     if ((e->policy & engine_policy_self_gravity) &&
         (type & proxy_cell_type_gravity))
       engine_addtasks_recv_gravity(e, ci, /*t_grav=*/NULL, tend);
-
-    /* Add the recv tasks for the cells in the proxy that have an RT
-     * connection. */
-    if ((e->policy & engine_policy_rt) && (type & proxy_cell_type_hydro))
-      engine_addtasks_recv_rt(e, ci, /*t_rt_gradient=*/NULL,
-                              /*t_rt_transport=*/NULL, tend);
   }
 }
 
diff --git a/src/engine_marktasks.c b/src/engine_marktasks.c
index 73c31310d9786b51e16c43df584f9d60f335a6be..25409d9cdd2fb901052a13898cbcd876589ce605 100644
--- a/src/engine_marktasks.c
+++ b/src/engine_marktasks.c
@@ -94,6 +94,7 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
 #ifdef SWIFT_DEBUG_CHECKS
       if (ci->nodeID != nodeID) error("Non-local self task found");
 #endif
+
       const int ci_active_hydro = cell_is_active_hydro(ci, e);
       const int ci_active_gravity = cell_is_active_gravity(ci, e);
       const int ci_active_black_holes = cell_is_active_black_holes(ci, e);
@@ -101,6 +102,7 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
           cell_is_active_sinks(ci, e) || ci_active_hydro;
       const int ci_active_stars = cell_need_activating_stars(
           ci, e, with_star_formation, with_star_formation_sink);
+      const int ci_active_rt = cell_is_rt_active(ci, e);
 
       /* Activate the hydro drift */
       if (t_type == task_type_self && t_subtype == task_subtype_density) {
@@ -345,9 +347,21 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
       }
 
       /* Activate RT tasks */
-      else if (t_subtype == task_subtype_rt_gradient ||
-               t_subtype == task_subtype_rt_transport) {
-        if (ci_active_hydro) scheduler_activate(s, t);
+      else if (t_type == task_type_self &&
+               t_subtype == task_subtype_rt_gradient) {
+        if (ci_active_rt) scheduler_activate(s, t);
+      }
+
+      else if (t_type == task_type_sub_self &&
+               t_subtype == task_subtype_rt_gradient) {
+        if (ci_active_rt) {
+          scheduler_activate(s, t);
+          cell_activate_subcell_rt_tasks(ci, NULL, s, /*sub_cycle=*/0);
+        }
+      }
+
+      else if (t_subtype == task_subtype_rt_transport) {
+        if (ci_active_rt) scheduler_activate(s, t);
       }
 
 #ifdef SWIFT_DEBUG_CHECKS
@@ -389,6 +403,9 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
       const int cj_active_stars = cell_need_activating_stars(
           cj, e, with_star_formation, with_star_formation_sink);
 
+      const int ci_active_rt = cell_is_rt_active(ci, e);
+      const int cj_active_rt = cell_is_rt_active(cj, e);
+
       /* Only activate tasks that involve a local active cell. */
       if ((t_subtype == task_subtype_density ||
            t_subtype == task_subtype_gradient ||
@@ -760,16 +777,33 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
 
       /* RT gradient and transport tasks */
       else if (t_subtype == task_subtype_rt_gradient) {
+
         /* We only want to activate the task if the cell is active and is
            going to update some gas on the *local* node */
 
-        if ((ci_nodeID == nodeID && ci_active_hydro) ||
-            (cj_nodeID == nodeID && cj_active_hydro)) {
+        if ((ci_nodeID == nodeID && ci_active_rt) ||
+            (cj_nodeID == nodeID && cj_active_rt)) {
 
-          /* The gradient and transport task subtypes mirror the hydro tasks.
-           * Therefore all the (subcell) sorts and drifts should already have
-           * been activated properly in the hydro part of the activation. */
           scheduler_activate(s, t);
+
+          /* Set the correct sorting flags */
+          if (t_type == task_type_pair) {
+
+            /* Store some values. */
+            atomic_or(&ci->hydro.requires_sorts, 1 << t->flags);
+            atomic_or(&cj->hydro.requires_sorts, 1 << t->flags);
+            ci->hydro.dx_max_sort_old = ci->hydro.dx_max_sort;
+            cj->hydro.dx_max_sort_old = cj->hydro.dx_max_sort;
+
+            /* Check the sorts and activate them if needed. */
+            cell_activate_rt_sorts(ci, t->flags, s);
+            cell_activate_rt_sorts(cj, t->flags, s);
+          }
+
+          /* Store current values of dx_max and h_max. */
+          else if (t_type == task_type_sub_pair) {
+            cell_activate_subcell_rt_tasks(ci, cj, s, /*sub_cycle=*/0);
+          }
         }
       }
 
@@ -777,8 +811,8 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
         /* We only want to activate the task if the cell is active and is
            going to update some gas on the *local* node */
 
-        if ((ci_nodeID == nodeID && ci_active_hydro) ||
-            (cj_nodeID == nodeID && cj_active_hydro)) {
+        if ((ci_nodeID == nodeID && ci_active_rt) ||
+            (cj_nodeID == nodeID && cj_active_rt)) {
 
           /* The gradient and transport task subtypes mirror the hydro tasks.
            * Therefore all the (subcell) sorts and drifts should already have
@@ -789,9 +823,9 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
             /* Activate transport_out for each cell that is part of
              * a pair/sub_pair task as to not miss any dependencies */
             if (ci_nodeID == nodeID)
-              scheduler_activate(s, ci->hydro.super->hydro.rt_transport_out);
+              scheduler_activate(s, ci->hydro.super->rt.rt_transport_out);
             if (cj_nodeID == nodeID)
-              scheduler_activate(s, cj->hydro.super->hydro.rt_transport_out);
+              scheduler_activate(s, cj->hydro.super->rt.rt_transport_out);
           }
         }
       }
@@ -1177,28 +1211,25 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
         if (ci_nodeID != nodeID) {
 
           /* If the local cell is active, receive data from the foreign cell. */
-          if (cj_active_hydro) {
+          if (cj_active_rt) {
+
             scheduler_activate_recv(s, ci->mpi.recv, task_subtype_rt_gradient);
 
-            if (ci_active_hydro) {
-              /* We only need updates later on if the other cell is active as
-               * well */
+            if (ci_active_rt) {
+              /* We only need updates later on if the other cell is active too
+               */
               scheduler_activate_recv(s, ci->mpi.recv,
                                       task_subtype_rt_transport);
             }
           }
 
           /* Is the foreign cell active and will need stuff from us? */
-          if (ci_active_hydro) {
+          if (ci_active_rt) {
 
             scheduler_activate_send(s, cj->mpi.send, task_subtype_rt_gradient,
                                     ci_nodeID);
 
-            /* Drift the cell which will be sent; note that not all sent
-               particles will be drifted, only those that are needed. */
-            cell_activate_drift_part(cj, s);
-
-            if (cj_active_hydro) {
+            if (cj_active_rt) {
               scheduler_activate_send(s, cj->mpi.send,
                                       task_subtype_rt_transport, ci_nodeID);
             }
@@ -1207,30 +1238,27 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
         } else if (cj_nodeID != nodeID) {
 
           /* If the local cell is active, receive data from the foreign cell. */
-          if (ci_active_hydro) {
+          if (ci_active_rt) {
+
             scheduler_activate_recv(s, cj->mpi.recv, task_subtype_rt_gradient);
 
-            if (cj_active_hydro) {
-              /* We only need updates later on if the other cell is active as
-               * well */
+            if (cj_active_rt) {
+              /* We only need updates later on if the other cell is active too
+               */
               scheduler_activate_recv(s, cj->mpi.recv,
                                       task_subtype_rt_transport);
             }
           }
 
           /* Is the foreign cell active and will need stuff from us? */
-          if (cj_active_hydro) {
+          if (cj_active_rt) {
 
             scheduler_activate_send(s, ci->mpi.send, task_subtype_rt_gradient,
                                     cj_nodeID);
 
-            /* Drift the cell which will be sent; note that not all sent
-               particles will be drifted, only those that are needed. */
-            cell_activate_drift_part(ci, s);
-
-            if (ci_active_hydro) {
-              /* We only need updates later on if the other cell is active as
-               * well */
+            if (ci_active_rt) {
+              /* We only need updates later on if the other cell is active too
+               */
               scheduler_activate_send(s, ci->mpi.send,
                                       task_subtype_rt_transport, cj_nodeID);
             }
@@ -1397,7 +1425,7 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
 
     /* Radiative transfer implicit tasks */
     else if (t->type == task_type_rt_in) {
-      if (cell_is_active_hydro(t->ci, e) ||
+      if (cell_is_rt_active(t->ci, e) ||
           cell_need_activating_stars(t->ci, e, with_star_formation,
                                      with_star_formation_sink))
         scheduler_activate(s, t);
@@ -1405,8 +1433,12 @@ void engine_marktasks_mapper(void *map_data, int num_elements,
 
     else if (t->type == task_type_rt_ghost1 || t->type == task_type_rt_ghost2 ||
              t->type == task_type_rt_transport_out ||
-             t->type == task_type_rt_tchem || t->type == task_type_rt_out) {
-      if (cell_is_active_hydro(t->ci, e)) scheduler_activate(s, t);
+             t->type == task_type_rt_tchem ||
+             t->type == task_type_rt_advance_cell_time ||
+             t->type == task_type_rt_out) {
+      if (cell_is_rt_active(t->ci, e)) scheduler_activate(s, t);
+      /* Note that rt_collect_times never needs to be active on main steps,
+       * which is always what follows engine_marktasks().*/
     }
 
     /* Subgrid tasks: sink formation */
diff --git a/src/engine_unskip.c b/src/engine_unskip.c
index 4fe4fa8de4211a41a2e35761d01e6689570bc39e..993603c2a3800cfb2dba364791d0ee6c5d68153e 100644
--- a/src/engine_unskip.c
+++ b/src/engine_unskip.c
@@ -247,8 +247,10 @@ static void engine_do_unskip_gravity(struct cell *c, struct engine *e) {
  *
  * @param c The cell.
  * @param e The engine.
+ * @param sub_cycle 1 if this is a call for a sub cycle, 0 otherwise
  */
-static void engine_do_unskip_rt(struct cell *c, struct engine *e) {
+static void engine_do_unskip_rt(struct cell *c, struct engine *e,
+                                const int sub_cycle) {
 
   /* Note: we only get this far if engine_policy_rt is flagged. */
 #ifdef SWIFT_DEBUG_CHECKS
@@ -260,20 +262,20 @@ static void engine_do_unskip_rt(struct cell *c, struct engine *e) {
   if (!cell_get_flag(c, cell_flag_has_tasks)) return;
 
   /* Do we have work to do? */
-  if (!cell_is_active_hydro(c, e)) return;
+  if (c->hydro.count == 0) return;
+  if (!cell_is_rt_active(c, e)) return;
 
   /* Recurse */
   if (c->split) {
     for (int k = 0; k < 8; k++) {
       if (c->progeny[k] != NULL) {
-        struct cell *cp = c->progeny[k];
-        engine_do_unskip_rt(cp, e);
+        engine_do_unskip_rt(c->progeny[k], e, sub_cycle);
       }
     }
   }
 
   /* Unskip any active tasks. */
-  const int forcerebuild = cell_unskip_rt_tasks(c, &e->sched);
+  const int forcerebuild = cell_unskip_rt_tasks(c, &e->sched, sub_cycle);
   if (forcerebuild) atomic_inc(&e->forcerebuild);
 }
 
@@ -364,7 +366,7 @@ void engine_do_unskip_mapper(void *map_data, int num_elements,
         if (!(e->policy & engine_policy_rt))
           error("Trying to unskip radiative transfer tasks in a non-rt run!");
 #endif
-        engine_do_unskip_rt(c, e);
+        engine_do_unskip_rt(c, e, /*sub_cycle=*/0);
         break;
       default:
 #ifdef SWIFT_DEBUG_CHECKS
@@ -418,7 +420,7 @@ void engine_unskip(struct engine *e) {
         (with_stars && c->nodeID == nodeID && cell_is_active_stars(c, e)) ||
         (with_sinks && cell_is_active_sinks(c, e)) ||
         (with_black_holes && cell_is_active_black_holes(c, e)) ||
-        (with_rt && cell_is_active_hydro(c, e))) {
+        (with_rt && cell_is_rt_active(c, e))) {
 
       if (num_active_cells != k)
         memswap(&local_cells[k], &local_cells[num_active_cells], sizeof(int));
@@ -509,3 +511,59 @@ void engine_unskip(struct engine *e) {
     message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
             clocks_getunit());
 }
+
+void engine_do_unskip_sub_cycle_mapper(void *map_data, int num_elements,
+                                       void *extra_data) {
+
+  struct engine *e = (struct engine *)extra_data;
+  struct cell *const cells_top = e->s->cells_top;
+
+  /* The current chunk of active cells */
+  const int *const local_cells = (int *)map_data;
+
+  /* Loop over this thread's chunk of cells to unskip */
+  for (int ind = 0; ind < num_elements; ind++) {
+
+    /* Handle on the cell */
+    struct cell *const c = &cells_top[local_cells[ind]];
+
+    engine_do_unskip_rt(c, e, /*sub_cycle=*/1);
+  }
+}
+
+/**
+ * @brief Unskip all the RT tasks that are active during this sub-cycle.
+ *
+ * @param e The #engine.
+ */
+void engine_unskip_rt_sub_cycle(struct engine *e) {
+
+  const ticks tic = getticks();
+  struct space *s = e->s;
+  const int with_rt = e->policy & engine_policy_rt;
+
+  if (!with_rt) error("Unskipping sub-cycles when running without RT!");
+
+  int *local_cells = e->s->local_cells_with_tasks_top;
+  int num_active_cells = 0;
+  for (int k = 0; k < s->nr_local_cells_with_tasks; k++) {
+    struct cell *c = &s->cells_top[local_cells[k]];
+
+    if (c->hydro.count == 0) continue;
+
+    if (cell_is_rt_active(c, e)) {
+
+      if (num_active_cells != k)
+        memswap(&local_cells[k], &local_cells[num_active_cells], sizeof(int));
+      num_active_cells += 1;
+    }
+  }
+
+  /* Activate all the regular tasks */
+  threadpool_map(&e->threadpool, engine_do_unskip_sub_cycle_mapper, local_cells,
+                 num_active_cells, sizeof(int), /*chunk=*/1, e);
+
+  if (e->verbose)
+    message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
+            clocks_getunit());
+}
diff --git a/src/hydro/AnarchyPU/hydro_part.h b/src/hydro/AnarchyPU/hydro_part.h
index b8dead7e5003676047021a06e88824273d76bee8..7cfa3f1cce1a637bf020e0bcda1c36977e2e2304 100644
--- a/src/hydro/AnarchyPU/hydro_part.h
+++ b/src/hydro/AnarchyPU/hydro_part.h
@@ -229,6 +229,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Gadget2/hydro_part.h b/src/hydro/Gadget2/hydro_part.h
index d5bf88312c61aafd6f3dc81e2f7990d80e167e94..a2ad80a1da696a458467288586e59bcd59576ab9 100644
--- a/src/hydro/Gadget2/hydro_part.h
+++ b/src/hydro/Gadget2/hydro_part.h
@@ -189,6 +189,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Gasoline/hydro_part.h b/src/hydro/Gasoline/hydro_part.h
index d2de6f8c772c366e2edba5afc3cdad23480fda56..39ebe556ee8eee33eb156bc39a30a050f1b4911e 100644
--- a/src/hydro/Gasoline/hydro_part.h
+++ b/src/hydro/Gasoline/hydro_part.h
@@ -237,6 +237,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Gizmo/MFM/hydro_part.h b/src/hydro/Gizmo/MFM/hydro_part.h
index d89bb4ecffa136bc6e2bcee3b030c0ce824657ee..097cf389d0e8a2e7081c377e7942e998603427b6 100644
--- a/src/hydro/Gizmo/MFM/hydro_part.h
+++ b/src/hydro/Gizmo/MFM/hydro_part.h
@@ -178,6 +178,12 @@ struct part {
   /*! Sink information (e.g. swallowing ID) */
   struct sink_part_data sink_data;
 
+  /*! Additional Radiative Transfer Data */
+  struct rt_part_data rt_data;
+
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Gizmo/MFV/hydro_part.h b/src/hydro/Gizmo/MFV/hydro_part.h
index 97003d2bcda3782a68b354d4c940f4dc0cc7b10c..cf88e1167bbb980378427588045d6de922c86231 100644
--- a/src/hydro/Gizmo/MFV/hydro_part.h
+++ b/src/hydro/Gizmo/MFV/hydro_part.h
@@ -182,6 +182,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Minimal/hydro_part.h b/src/hydro/Minimal/hydro_part.h
index a3d12ad665d0a183458b1dc1e04ee0e8cab45837..6709bb5e236bfd1f3ab1cffb6ecdc925115a9375 100644
--- a/src/hydro/Minimal/hydro_part.h
+++ b/src/hydro/Minimal/hydro_part.h
@@ -207,6 +207,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/None/hydro_part.h b/src/hydro/None/hydro_part.h
index c6270a53cdf743a3d8f131e74b42dac5615aab89..589c874e9716bb17c97d14da6d6e202ccffa6439 100644
--- a/src/hydro/None/hydro_part.h
+++ b/src/hydro/None/hydro_part.h
@@ -173,6 +173,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Phantom/hydro_part.h b/src/hydro/Phantom/hydro_part.h
index 9a1dfd80a10a23299f16b08057a9cba45e7151cb..7c65800e3ccca7c069a993de95733163cb3bac45 100644
--- a/src/hydro/Phantom/hydro_part.h
+++ b/src/hydro/Phantom/hydro_part.h
@@ -227,6 +227,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Planetary/hydro_part.h b/src/hydro/Planetary/hydro_part.h
index c82739355db5c82b3124002273c9429b87601c51..ccc7ef3b9996ccd544a5b27af0eaca0f052b69c8 100644
--- a/src/hydro/Planetary/hydro_part.h
+++ b/src/hydro/Planetary/hydro_part.h
@@ -209,6 +209,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/PressureEnergy/hydro_part.h b/src/hydro/PressureEnergy/hydro_part.h
index 5624c92135ea22d42c3fbd4f9cf2e07963f4a242..75d5df1835bde62933b590d0606ec7a60a51a130 100644
--- a/src/hydro/PressureEnergy/hydro_part.h
+++ b/src/hydro/PressureEnergy/hydro_part.h
@@ -214,6 +214,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h
index 40211ed19aeeffe06e8d78814a3dc05b3fcd5bf4..aae8ac721e462c916e003b3086ab127c76a928d7 100644
--- a/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h
+++ b/src/hydro/PressureEnergyMorrisMonaghanAV/hydro_part.h
@@ -211,6 +211,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/PressureEntropy/hydro_part.h b/src/hydro/PressureEntropy/hydro_part.h
index 53100336a98a4789cc9d8f33d26c2466faad4d16..8665b70098f98df1dbb81a2a0f3dca34b2a21eeb 100644
--- a/src/hydro/PressureEntropy/hydro_part.h
+++ b/src/hydro/PressureEntropy/hydro_part.h
@@ -187,6 +187,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/SPHENIX/hydro_part.h b/src/hydro/SPHENIX/hydro_part.h
index 682a0dcc3be3febb9f950f86aa5862ef62be199f..bbc3bd717d205e5799bce97561fdf757e421a836 100644
--- a/src/hydro/SPHENIX/hydro_part.h
+++ b/src/hydro/SPHENIX/hydro_part.h
@@ -239,6 +239,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/hydro/Shadowswift/hydro_part.h b/src/hydro/Shadowswift/hydro_part.h
index efecb8bee1e27618bbffbcf47240369658310f2b..397dba20d6b0d05fd473f6c1dc9c519cb8777493 100644
--- a/src/hydro/Shadowswift/hydro_part.h
+++ b/src/hydro/Shadowswift/hydro_part.h
@@ -206,6 +206,9 @@ struct part {
   /*! Additional Radiative Transfer Data */
   struct rt_part_data rt_data;
 
+  /*! RT sub-cycling time stepping data */
+  struct rt_timestepping_data rt_time_data;
+
   /*! Time-step length */
   timebin_t time_bin;
 
diff --git a/src/rt/GEAR/rt.h b/src/rt/GEAR/rt.h
index 13afdb485a06b4366f4b8d2ecad5cd2700616745..d84c27adb87bfb6b40620b43e92e340417243646 100644
--- a/src/rt/GEAR/rt.h
+++ b/src/rt/GEAR/rt.h
@@ -87,11 +87,10 @@ __attribute__((always_inline)) INLINE static void rt_init_part(
     struct part* restrict p) {}
 
 /**
- * @brief Reset of the RT hydro particle data not related to the density.
+ * @brief Reset the RT hydro particle data not related to the hydro density.
  * Note: during initalisation (space_init), rt_reset_part and rt_init_part
- * are both called individually. Also, if debugging checks are active, an
- * extra call to rt_reset_part is made in
- * space_convert_rt_quantities_after_zeroth_step().
+ * are both called individually. To reset RT data needed in each RT sub-cycle,
+ * use rt_reset_part_each_subcycle().
  *
  * @param p particle to work on
  * @param cosmo Cosmology.
@@ -103,15 +102,21 @@ __attribute__((always_inline)) INLINE static void rt_reset_part(
   /* reset this here as well as in the rt_debugging_checks_end_of_step()
    * routine to test task dependencies are done right */
   p->rt_data.debug_iact_stars_inject = 0;
+  p->rt_data.debug_nsubcycles = 0;
+  p->rt_data.debug_kicked = 0;
+#endif
+}
 
-  p->rt_data.debug_calls_iact_gradient_interaction = 0;
-  p->rt_data.debug_calls_iact_transport_interaction = 0;
+/**
+ * @brief Reset RT particle data which needs to be reset each sub-cycle.
+ *
+ * @param p the particle to work on
+ */
+__attribute__((always_inline)) INLINE static void rt_reset_part_each_subcycle(
+    struct part* restrict p) {
 
-  p->rt_data.debug_kicked = 0;
-  p->rt_data.debug_injection_done = 0;
-  p->rt_data.debug_gradients_done = 0;
-  p->rt_data.debug_transport_done = 0;
-  p->rt_data.debug_thermochem_done = 0;
+#ifdef SWIFT_RT_DEBUG_CHECKS
+  rt_debugging_reset_each_subcycle(p);
 #endif
 
   rt_gradients_init(p);
@@ -136,41 +141,13 @@ __attribute__((always_inline)) INLINE static void rt_first_init_part(
   rt_init_part(p);
   rt_reset_part(p, cosmo);
   rt_part_reset_mass_fluxes(p);
+  rt_reset_part_each_subcycle(p);
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
   p->rt_data.debug_radiation_absorbed_tot = 0ULL;
 #endif
 }
 
-/**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the particle density and time step to be known.
- *
- * @param p particle to work on
- * @param rt_props RT properties struct
- * @param cosmo #cosmology data structure.
- */
-__attribute__((always_inline)) INLINE static void
-rt_init_part_after_zeroth_step(struct part* restrict p,
-                               const struct rt_props* rt_props,
-                               const struct cosmology* restrict cosmo) {
-
-#ifdef SWIFT_RT_DEBUG_CHECKS
-  /* If we're running with debugging checks on, reset debugging
-   * counters and flags in particular after the zeroth step so
-   * that the checks work as intended. */
-  rt_init_part(p);
-  rt_reset_part(p, cosmo);
-  /* Since the inject_prep has been moved to the density loop, the
-   * initialization at startup is messing with the total counters for stars
-   * because the density is called, but not the force-and-kick tasks. So reset
-   * the total counters here as well so that they will match the star counters.
-   */
-  p->rt_data.debug_radiation_absorbed_tot = 0ULL;
-#endif
-}
-
 /**
  * @brief Initialisation of the RT density loop related star particle data.
  * Note: during initalisation (space_init), rt_reset_spart and rt_init_spart
@@ -205,9 +182,7 @@ __attribute__((always_inline)) INLINE static void rt_init_spart(
 /**
  * @brief Reset of the RT star particle data not related to the density.
  * Note: during initalisation (space_init), rt_reset_spart and rt_init_spart
- * are both called individually. Also, if debugging checks are active, an
- * extra call to rt_reset_spart is made in
- * space_convert_rt_quantities_after_zeroth_step()
+ * are both called individually.
  *
  * @param sp star particle to work on
  */
@@ -237,39 +212,6 @@ __attribute__((always_inline)) INLINE static void rt_first_init_spart(
 #endif
 }
 
-/**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the star density and time step to be known.
- * @param sp star particle to work on
- * @param time current system time
- * @param star_age age of the star *at the end of the step*
- * @param dt star time step
- * @param rt_props RT properties struct
- * @param phys_const physical constants struct
- * @param internal_units struct holding internal units
- */
-__attribute__((always_inline)) INLINE static void
-rt_init_star_after_zeroth_step(struct spart* restrict sp, double time,
-                               double star_age, double dt,
-                               const struct rt_props* rt_props,
-                               const struct phys_const* phys_const,
-                               const struct unit_system* internal_units) {
-
-#ifdef SWIFT_RT_DEBUG_CHECKS
-  /* If we're running with debugging checks on, reset debugging
-   * counters and flags in particular after the zeroth step so
-   * that the checks work as intended. */
-  rt_init_spart(sp);
-  rt_reset_spart(sp);
-  /* Since the inject_prep has been moved to the density loop, the
-   * initialization at startup is messing with the total counters because
-   * the density is called, but not the force-and-kick tasks. So reset
-   * the total counters here as well. */
-  sp->rt_data.debug_radiation_emitted_tot = 0ULL;
-#endif
-}
-
 /**
  * @brief Split the RT data of a particle into n pieces
  *
@@ -385,6 +327,8 @@ __attribute__((always_inline)) INLINE static float rt_compute_timestep(
   float dt = psize * rt_params.reduced_speed_of_light_inverse *
              rt_props->CFL_condition;
 
+  if (rt_props->skip_thermochemistry) return dt;
+
   float dt_cool = FLT_MAX;
   if (rt_props->f_limit_cooling_time > 0.f)
     /* Note: cooling time may be negative if the gas is being heated */
@@ -448,9 +392,8 @@ __attribute__((always_inline)) INLINE static void rt_finalise_injection(
     struct part* restrict p, struct rt_props* props) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  if (p->rt_data.debug_kicked != 1)
-    error("called rt_ghost1 when particle %lld is unkicked (count=%d)", p->id,
-          p->rt_data.debug_kicked);
+  rt_debug_sequence_check(p, 1, "rt_ghost1/rt_finalise_injection");
+
   p->rt_data.debug_injection_done += 1;
 #endif
 
@@ -471,15 +414,7 @@ __attribute__((always_inline)) INLINE static void rt_end_gradient(
     struct part* restrict p, const struct cosmology* cosmo) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  if (p->rt_data.debug_kicked != 1)
-    error("called finalise gradient when particle %lld is unkicked (count=%d)",
-          p->id, p->rt_data.debug_kicked);
-
-  if (p->rt_data.debug_injection_done != 1)
-    error(
-        "Called finalise gradient on particle %lld"
-        "where injection_done count = %d",
-        p->id, p->rt_data.debug_injection_done);
+  rt_debug_sequence_check(p, 2, __func__);
 
   if (p->rt_data.debug_calls_iact_gradient_interaction == 0)
     message(
@@ -505,21 +440,7 @@ __attribute__((always_inline)) INLINE static void rt_finalise_transport(
     const struct cosmology* restrict cosmo) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  if (p->rt_data.debug_kicked != 1)
-    error("called finalise transport when particle %lld is unkicked (count=%d)",
-          p->id, p->rt_data.debug_kicked);
-
-  if (p->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do finalise_transport on particle %lld when "
-        "injection_done count is %d",
-        p->id, p->rt_data.debug_injection_done);
-
-  if (p->rt_data.debug_gradients_done != 1)
-    error(
-        "Trying to do finalise_transport on particle %lld when "
-        "gradients_done count is %d",
-        p->id, p->rt_data.debug_gradients_done);
+  rt_debug_sequence_check(p, 3, __func__);
 
   if (p->rt_data.debug_calls_iact_transport_interaction == 0)
     message(
@@ -568,21 +489,7 @@ __attribute__((always_inline)) INLINE static void rt_tchem(
     const struct unit_system* restrict us, const double dt) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  if (p->rt_data.debug_kicked != 1)
-    error(
-        "Part %lld trying to do thermochemistry on unkicked particle "
-        "(count=%d)",
-        p->id, p->rt_data.debug_kicked);
-  if (p->rt_data.debug_injection_done != 1)
-    error("Part %lld trying to do thermochemistry when injection_done != 1: %d",
-          p->id, p->rt_data.debug_injection_done);
-  if (p->rt_data.debug_gradients_done != 1)
-    error("Part %lld trying to do thermochemistry when gradients_done != 1: %d",
-          p->id, p->rt_data.debug_gradients_done);
-  if (p->rt_data.debug_transport_done != 1)
-    error("Part %lld trying to do thermochemistry when transport_done != 1: %d",
-          p->id, p->rt_data.debug_transport_done);
-
+  rt_debug_sequence_check(p, 4, __func__);
   p->rt_data.debug_thermochem_done += 1;
 #endif
 
@@ -595,7 +502,7 @@ __attribute__((always_inline)) INLINE static void rt_tchem(
 
 /**
  * @brief Extra operations done during the kick. This needs to be
- * done before the particle mass is updated in the hydro_kick_extra
+ * done before the particle mass is updated in the hydro_kick_extra.
  *
  * @param p Particle to act upon.
  * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
@@ -615,6 +522,8 @@ __attribute__((always_inline)) INLINE static void rt_kick_extra(
   /* Don't account for timestep_sync backward kicks */
   if (dt_therm >= 0.f && dt_grav >= 0.f && dt_hydro >= 0.f &&
       dt_kick_corr >= 0.f) {
+
+    rt_debug_sequence_check(p, 0, __func__);
     p->rt_data.debug_kicked += 1;
   }
 #endif
diff --git a/src/rt/GEAR/rt_debugging.h b/src/rt/GEAR/rt_debugging.h
index 5421dde78016efa4c1a512108d6389429bf46dd6..dc62a200a51495af1bd4b6b9ef0251a2ccf07c29 100644
--- a/src/rt/GEAR/rt_debugging.h
+++ b/src/rt/GEAR/rt_debugging.h
@@ -21,7 +21,9 @@
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
 
+#include "active.h"
 #include "rt_properties.h"
+#include "timeline.h"
 
 /**
  * @file src/rt/GEAR/rt_debugging.h
@@ -29,6 +31,100 @@
  * extra debugging functions.
  */
 
+/**
+ * @brief This resets particle carried quantities after each subcycling
+ * step such that the internal checks are still consistent.
+ * @param p the particle to work on
+ */
+__attribute__((always_inline)) INLINE static void rt_debugging_count_subcycle(
+    struct part *restrict p) {
+  p->rt_data.debug_nsubcycles += 1;
+}
+
+/**
+ * @brief Check that the particle completed the correct number of subcycles.
+ * This is checked in every rt_reset_part, before the subcycling count is reset.
+ * @param p the particle to work on
+ * @param rt_props RT properties struct
+ */
+__attribute__((always_inline)) INLINE static void
+rt_debugging_check_nr_subcycles(struct part *restrict p,
+                                const struct rt_props *rt_props) {
+
+  /* TODO: this check may fail when running with limiter/sync. */
+
+  /* NOTE: we need to do this check somewhere in the hydro tasks.
+   * (1) it needs to be done when all tasks are active and before the
+   * particle hydro time step is changed.
+   * (2) If you do it during RT tasks, it won't properly check how
+   * many sub-cycles you did during a single hydro task.
+   * (3) You can't do it during the timestep task, since between
+   * the hydro and the timestep we already do an RT step. */
+
+  /* skip initialization */
+  if (p->time_bin == 0) return;
+  if (p->rt_time_data.time_bin == 0)
+    error("Got part %lld with RT time bin 0", p->id);
+
+  timebin_t bindiff = p->time_bin - p->rt_time_data.time_bin;
+
+  if (rt_props->debug_max_nr_subcycles <= 1) {
+    /* Running without subcycling. */
+    if (bindiff != 0)
+      error("Running without subcycling but got bindiff=%d for part=%lld",
+            bindiff, p->id);
+    if (p->rt_data.debug_nsubcycles != 1)
+      error("Running without subcycling but got part=%lld subcycle count=%d",
+            p->id, p->rt_data.debug_nsubcycles);
+    return;
+  }
+
+  /* TODO: this assumes that max_nr_subcycles is not an upper limit,
+   * but a fixed number of sub-cycles */
+  timebin_t bindiff_expect = 0;
+
+  while (!(rt_props->debug_max_nr_subcycles & (1 << bindiff_expect)) &&
+         bindiff_expect != num_time_bins)
+    ++bindiff_expect;
+
+  if (bindiff_expect == num_time_bins)
+    error(
+        "Couldn't determine expected time bin difference. Max nr subcycles %d "
+        "bindiff %d",
+        rt_props->debug_max_nr_subcycles, bindiff);
+
+  if (bindiff != bindiff_expect)
+    error("Particle %lld Got bindiff=%d expect=%d; timebins=%d rt=%d", p->id,
+          bindiff, bindiff_expect, p->time_bin, p->rt_time_data.time_bin);
+
+  int subcycles_expect = (1 << bindiff);
+  if (p->rt_data.debug_nsubcycles != subcycles_expect)
+
+    if (p->rt_data.debug_nsubcycles != rt_props->debug_max_nr_subcycles)
+      error(
+          "Particle %lld didn't do the expected amount of subcycles: Expected "
+          "%d, done %d; time bins %d RT: %d",
+          p->id, subcycles_expect, p->rt_data.debug_nsubcycles, p->time_bin,
+          p->rt_time_data.time_bin);
+}
+
+/**
+ * @brief This resets particle carried quantities after each subcycling
+ * step such that the internal checks are still consistent.
+ * @param p the particle to work on
+ */
+__attribute__((always_inline)) INLINE static void
+rt_debugging_reset_each_subcycle(struct part *restrict p) {
+
+  p->rt_data.debug_calls_iact_gradient_interaction = 0;
+  p->rt_data.debug_calls_iact_transport_interaction = 0;
+
+  p->rt_data.debug_injection_done = 0;
+  p->rt_data.debug_gradients_done = 0;
+  p->rt_data.debug_transport_done = 0;
+  p->rt_data.debug_thermochem_done = 0;
+}
+
 /**
  * @brief Debugging checks loop over all star particles after each time step
  */
@@ -53,6 +149,9 @@ static void rt_debugging_end_of_step_stars_mapper(void *restrict map_data,
     sp->rt_data.debug_iact_hydro_inject = 0;
     sp->rt_data.debug_iact_hydro_inject_prep = 0;
 
+#ifndef WITH_MPI
+    /* These checks will fail with MPI since we're not
+     * sending data back */
     for (int g = 0; g < RT_NGROUPS; g++) {
       /* also check now that we actually injected the correct
        * amount of energy
@@ -84,6 +183,7 @@ static void rt_debugging_end_of_step_stars_mapper(void *restrict map_data,
       }
       emitted_energy[g] += sp->rt_data.debug_injected_energy[g];
     }
+#endif
 
     for (int g = 0; g < RT_NGROUPS; g++) {
       sp->rt_data.debug_injected_energy[g] = 0.f;
@@ -121,6 +221,7 @@ static void rt_debugging_end_of_step_hydro_mapper(void *restrict map_data,
     struct part *restrict p = &parts[k];
     absorption_sum_this_step += p->rt_data.debug_iact_stars_inject;
     absorption_sum_tot += p->rt_data.debug_radiation_absorbed_tot;
+
     /* Reset all values here in case particles won't be active next step */
     p->rt_data.debug_iact_stars_inject = 0;
 
@@ -154,11 +255,6 @@ rt_debugging_checks_end_of_step(struct engine *e, int verbose) {
 
   struct space *s = e->s;
   if (!(e->policy & engine_policy_rt)) return;
-#ifdef WITH_MPI
-  /* Since we aren't sending data back, none of these checks will
-   * pass a run over MPI. */
-  return;
-#endif
 
   const ticks tic = getticks();
 
@@ -186,7 +282,15 @@ rt_debugging_checks_end_of_step(struct engine *e, int verbose) {
                    s->sparts, s->nr_sparts, sizeof(struct spart),
                    threadpool_auto_chunk_size, /*extra_data=*/e);
 
+#ifdef WITH_MPI
+  /* Since we aren't sending data back, none of these checks will
+   * pass a run over MPI. Make sure you run the threadpool functions
+   * first though so certain variables can get reset properly. */
+  return;
+#endif
+
   /* Have we accidentally invented or deleted some radiation somewhere? */
+
   if ((e->rt_props->debug_radiation_emitted_this_step !=
        e->rt_props->debug_radiation_absorbed_this_step) ||
       (e->rt_props->debug_radiation_emitted_tot !=
@@ -229,5 +333,79 @@ rt_debugging_checks_end_of_step(struct engine *e, int verbose) {
             clocks_getunit());
 }
 
+/**
+ * @brief Perform a series of consistency and sanity checks.
+ *
+ * @param p particle to check
+ * @param loc location where this is called from. This determines which checks
+ * will be done:
+ *
+ * 0: during kicks/after drifts.
+ * 1: during rt_ghost1/finalise_injection / after kicks.
+ * 2: during gradients / after injection.
+ * 3: during transport / after gradients.
+ * 4: during thermochem / after transport.
+ * 5: after thermochem.
+ *
+ * @param function_name: Function name (or message) you want printed on error.
+ */
+__attribute__((always_inline)) INLINE static void rt_debug_sequence_check(
+    struct part *restrict p, int loc, const char *function_name) {
+
+  /* Note: Checking whether a particle has been drifted at all is not
+   * compatible with subcycling. There is no reliable point where to
+   * reset the counters and have sensible results. */
+
+  if (loc > 0) {
+    /* Are kicks done? */
+    if (p->rt_data.debug_nsubcycles == 0) {
+      if (p->rt_data.debug_kicked != 1)
+        error(
+            "called %s on particle %lld with wrong kick count=%d (expected "
+            "1) cycle=%d",
+            function_name, p->id, p->rt_data.debug_kicked,
+            p->rt_data.debug_nsubcycles);
+    } else if (p->rt_data.debug_nsubcycles > 0) {
+      /* This covers case 1, 2, 3, 4, 5 */
+      if (p->rt_data.debug_kicked != 2)
+        error(
+            "called %s on particle %lld with wrong kick count=%d (expected 2) "
+            "cycle=%d",
+            function_name, p->id, p->rt_data.debug_kicked,
+            p->rt_data.debug_nsubcycles);
+    } else {
+      error("Got negative subcycle???");
+    }
+  }
+
+  if (loc > 1) {
+    /* is injection done? */
+    if (p->rt_data.debug_injection_done != 1)
+      error("called %s on part %lld when finalise injection count is %d ID",
+            function_name, p->id, p->rt_data.debug_injection_done);
+  }
+
+  if (loc > 2) {
+    /* are gradients done? */
+    if (p->rt_data.debug_gradients_done != 1)
+      error("called %s on part %lld when gradients_done count is %d",
+            function_name, p->id, p->rt_data.debug_gradients_done);
+  }
+
+  if (loc > 3) {
+    /* is transport done? */
+    if (p->rt_data.debug_transport_done != 1)
+      error("called %s on part %lld when transport_done != 1: %d",
+            function_name, p->id, p->rt_data.debug_transport_done);
+  }
+
+  if (loc > 4) {
+    /* is thermochemistry done? */
+    if (p->rt_data.debug_thermochem_done != 1)
+      error("called %s on part %lld with thermochem_done count=%d",
+            function_name, p->id, p->rt_data.debug_thermochem_done);
+  }
+}
+
 #endif /* SWIFT_RT_DEBUG_CHECKS */
 #endif /* SWIFT_RT_DEBUGGING_GEAR_H */
diff --git a/src/rt/GEAR/rt_gradients.h b/src/rt/GEAR/rt_gradients.h
index 2db091a80502248da3c66c4f9a17c6f57de3c9ff..26fc5e468b42cd045be4a6fa293146dfe45af996 100644
--- a/src/rt/GEAR/rt_gradients.h
+++ b/src/rt/GEAR/rt_gradients.h
@@ -147,29 +147,8 @@ __attribute__((always_inline)) INLINE static void rt_gradients_collect(
     struct part *restrict pj) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  if (pi->rt_data.debug_kicked != 1)
-    error(
-        "Trying to do symmetric iact gradient unkicked particle %lld "
-        "(count=%d)",
-        pi->id, pi->rt_data.debug_kicked);
-
-  if (pi->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do symmetric iact gradient when finalise injection count is "
-        "%d ID %lld",
-        pi->rt_data.debug_injection_done, pi->id);
-
-  if (pj->rt_data.debug_kicked != 1)
-    error(
-        "Trying to do symmetric iact gradient unkicked particle %lld "
-        "(count=%d)",
-        pj->id, pj->rt_data.debug_kicked);
-
-  if (pj->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do symmetric iact gradient when finalise injection count is "
-        "%d ID %lld",
-        pj->rt_data.debug_injection_done, pj->id);
+  rt_debug_sequence_check(pi, 2, __func__);
+  rt_debug_sequence_check(pj, 2, __func__);
 
   pi->rt_data.debug_calls_iact_gradient_interaction += 1;
   pj->rt_data.debug_calls_iact_gradient_interaction += 1;
@@ -307,18 +286,7 @@ __attribute__((always_inline)) INLINE static void rt_gradients_nonsym_collect(
     struct part *restrict pj) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  if (pi->rt_data.debug_kicked != 1)
-    error(
-        "Trying to do nonsym iact gradient on unkicked particle %lld "
-        "(count=%d)",
-        pi->id, pi->rt_data.debug_kicked);
-
-  if (pi->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do nonsym iact gradients when finalise injection count is "
-        "%d ID %lld",
-        pi->rt_data.debug_injection_done, pi->id);
-
+  rt_debug_sequence_check(pi, 2, __func__);
   pi->rt_data.debug_calls_iact_gradient_interaction += 1;
 #endif
 
diff --git a/src/rt/GEAR/rt_iact.h b/src/rt/GEAR/rt_iact.h
index 1f449d1758255ae6c765ea65a9c4ee46dd698bca..db9694179628b6ca61a81be6182e25ccdba683b5 100644
--- a/src/rt/GEAR/rt_iact.h
+++ b/src/rt/GEAR/rt_iact.h
@@ -19,6 +19,7 @@
 #ifndef SWIFT_RT_IACT_GEAR_H
 #define SWIFT_RT_IACT_GEAR_H
 
+#include "rt_debugging.h"
 #include "rt_flux.h"
 #include "rt_gradients.h"
 
@@ -225,42 +226,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_flux_common(
     struct part *restrict pj, float a, float H, int mode) {
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  if (pi->rt_data.debug_kicked != 1)
-    error("Trying to iact transport with unkicked particle %lld (count=%d)",
-          pi->id, pi->rt_data.debug_kicked);
-
-  if (pi->rt_data.debug_injection_done != 1)
-    error(
-        "Part %lld trying to do iact transport when "
-        "injection_done count is %d",
-        pi->id, pi->rt_data.debug_injection_done);
-
-  if (pi->rt_data.debug_gradients_done != 1)
-    error(
-        "Part %lld trying to do iact transport when "
-        "gradients_done count is %d",
-        pi->id, pi->rt_data.debug_gradients_done);
-
+  const char *func_name = (mode == 1) ? "sym flux iact" : "nonsym flux iact";
+  rt_debug_sequence_check(pi, 3, func_name);
   pi->rt_data.debug_calls_iact_transport_interaction += 1;
 
   if (mode == 1) {
-
-    if (pj->rt_data.debug_kicked != 1)
-      error("Trying to iact transport with unkicked particle %lld (count=%d)",
-            pj->id, pj->rt_data.debug_kicked);
-
-    if (pj->rt_data.debug_injection_done != 1)
-      error(
-          "Part %lld Trying to do iact transport when "
-          "injection_done count is %d",
-          pj->id, pj->rt_data.debug_injection_done);
-
-    if (pj->rt_data.debug_gradients_done != 1)
-      error(
-          "Part %lld Trying to do iact transport when "
-          "gradients_done count is %d",
-          pj->id, pj->rt_data.debug_gradients_done);
-
+    rt_debug_sequence_check(pj, 3, func_name);
     pj->rt_data.debug_calls_iact_transport_interaction += 1;
   }
 #endif
diff --git a/src/rt/GEAR/rt_io.h b/src/rt/GEAR/rt_io.h
index e67b73ed84c1375616edca071f5145e5822394ec..57b70efd02d66a16bf846d7e891057ee7d0fbc7e 100644
--- a/src/rt/GEAR/rt_io.h
+++ b/src/rt/GEAR/rt_io.h
@@ -175,7 +175,7 @@ INLINE static int rt_write_particles(const struct part* parts,
       "Mass fractions of all constituent species");
 
 #ifdef SWIFT_RT_DEBUG_CHECKS
-  num_elements += 7;
+  num_elements += 8;
   list[3] =
       io_make_output_field("RTDebugInjectionDone", INT, 1, UNIT_CONV_NO_UNITS,
                            0, parts, rt_data.debug_injection_done,
@@ -207,6 +207,9 @@ INLINE static int rt_write_particles(const struct part* parts,
       "RTDebugRadAbsorbedTot", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0, parts,
       rt_data.debug_radiation_absorbed_tot,
       "Radiation absorbed by this part during its lifetime");
+  list[10] = io_make_output_field(
+      "RTDebugSubcycles", INT, 1, UNIT_CONV_NO_UNITS, 0, parts,
+      rt_data.debug_nsubcycles, "How many times this part was subcycled");
 #endif
 
   return num_elements;
diff --git a/src/rt/GEAR/rt_properties.h b/src/rt/GEAR/rt_properties.h
index 414c901c640157be5618b6f3a253da3feefc5086..53c610fa02c72e0d4a09dd52b3f6d17e836e42eb 100644
--- a/src/rt/GEAR/rt_properties.h
+++ b/src/rt/GEAR/rt_properties.h
@@ -122,6 +122,9 @@ struct rt_props {
    * but a placeholder to sum up a global variable */
   unsigned long long debug_radiation_absorbed_tot;
 
+  /* Max number of subcycles per hydro step */
+  int debug_max_nr_subcycles;
+
   /* Total radiation energy in the gas. It's being reset every step. */
   float debug_total_radiation_conserved_energy[RT_NGROUPS];
   float debug_total_star_emitted_energy[RT_NGROUPS];
@@ -431,6 +434,11 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
   rtp->debug_radiation_absorbed_tot = 0ULL;
   rtp->debug_radiation_absorbed_this_step = 0ULL;
 
+  /* Don't make it an optional parameter here so we crash
+   * if I forgot to provide it */
+  rtp->debug_max_nr_subcycles =
+      parser_get_param_int(params, "TimeIntegration:max_nr_rt_subcycles");
+
   for (int g = 0; g < RT_NGROUPS; g++)
     rtp->debug_total_star_emitted_energy[g] = 0.f;
 
@@ -452,14 +460,6 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
 
   /* Finishers */
   /* --------- */
-
-  /* After initialisation, print params to screen */
-  rt_props_print(rtp);
-
-  /* Print a final message. */
-  if (engine_rank == 0) {
-    message("Radiative transfer initialized");
-  }
 }
 
 /**
diff --git a/src/rt/GEAR/rt_struct.h b/src/rt/GEAR/rt_struct.h
index b02d9f5f6054105fed150952acf5ff91e77901d2..17227015e77c9ef7ddb773fa75423c535b6ea736 100644
--- a/src/rt/GEAR/rt_struct.h
+++ b/src/rt/GEAR/rt_struct.h
@@ -115,6 +115,11 @@ struct rt_part_data {
   /*! thermochemistry done? */
   int debug_thermochem_done;
 
+  /* Subcycling flags */
+
+  /*! Current subcycle wrt (last) hydro step */
+  int debug_nsubcycles;
+
 #endif
 };
 
diff --git a/src/rt/GEAR/rt_unphysical.h b/src/rt/GEAR/rt_unphysical.h
index a4817c56705101929d99e92a66051ddb54b8ec10..9df63cd11873554d89208f390adec3c68a855dcf 100644
--- a/src/rt/GEAR/rt_unphysical.h
+++ b/src/rt/GEAR/rt_unphysical.h
@@ -46,7 +46,8 @@ __attribute__((always_inline)) INLINE static void rt_check_unphysical_state(
   float ratio = 2.;
   if (e_old != 0.f) ratio = fabsf(*energy_density / e_old);
   /* callloc = 1 is gradient extrapolation. Don't print out those. */
-  if (*energy_density < -1e-2f && fabsf(ratio - 1.f) > 1.e-3f && callloc != 1)
+  if (*energy_density < -1e-3f * fabsf(e_old) && fabsf(ratio - 1.f) > 1.e-3f &&
+      callloc != 1)
     message("Fixing unphysical energy case %d | %.6e | %.6e %.6e %.6e | %.6e",
             callloc, *energy_density, flux[0], flux[1], flux[2], ratio);
 #endif
diff --git a/src/rt/SPHM1RT/rt.h b/src/rt/SPHM1RT/rt.h
index 074a67d89ee7a90229a46f8e0022f18e1436b9df..f5b4460f55a29c90dc594ddee875caf0785e833d 100644
--- a/src/rt/SPHM1RT/rt.h
+++ b/src/rt/SPHM1RT/rt.h
@@ -46,9 +46,11 @@ __attribute__((always_inline)) INLINE static void rt_init_part(
     struct part* restrict p) {}
 
 /**
- * @brief Reset of the RT hydro particle data not related to the density.
+ * @brief Reset the RT hydro particle data not related to the hydro density.
  * Note: during initalisation (space_init), rt_reset_part and rt_init_part
- * are both called individually.
+ * are both called individually. To reset RT data needed in each RT sub-cycle,
+ * use rt_reset_part_each_subcycle().
+ *
  * @param p particle to work on
  * @param cosmo Cosmology.
  */
@@ -93,6 +95,14 @@ __attribute__((always_inline)) INLINE static void rt_reset_part(
   }
 }
 
+/**
+ * @brief Reset RT particle data which needs to be reset each sub-cycle.
+ *
+ * @param p the particle to work on
+ */
+__attribute__((always_inline)) INLINE static void rt_reset_part_each_subcycle(
+    struct part* restrict p){};
+
 /**
  * @brief First initialisation of the RT hydro particle data.
  *
@@ -124,20 +134,6 @@ __attribute__((always_inline)) INLINE static void rt_first_init_part(
   rt_reset_part(p, cosmo);
 }
 
-/**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the particle density and time step to be known.
- *
- * @param p particle to work on
- * @param rt_props RT properties struct
- * @param cosmo #cosmology data structure.
- */
-__attribute__((always_inline)) INLINE static void
-rt_init_part_after_zeroth_step(struct part* restrict p,
-                               const struct rt_props* rt_props,
-                               const struct cosmology* restrict cosmo) {}
-
 /**
  * @brief Initialisation of the RT density loop related star particle data.
  * Note: during initalisation (space_init), rt_reset_spart and rt_init_spart
@@ -178,25 +174,6 @@ __attribute__((always_inline)) INLINE static void rt_first_init_spart(
   rt_reset_spart(sp);
 }
 
-/**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the star density and time step to be known.
- * @param sp star particle to work on
- * @param time current system time
- * @param star_age age of the star *at the end of the step*
- * @param dt star time step
- * @param rt_props RT properties struct
- * @param phys_const physical constants struct
- * @param internal_units struct holding internal units
- */
-__attribute__((always_inline)) INLINE static void
-rt_init_star_after_zeroth_step(struct spart* restrict sp, double time,
-                               double star_age, double dt,
-                               const struct rt_props* rt_props,
-                               const struct phys_const* phys_const,
-                               const struct unit_system* internal_units) {}
-
 /**
  * @brief Split the RT data of a particle into n pieces
  *
diff --git a/src/rt/debug/rt.h b/src/rt/debug/rt.h
index b46b3baa703786cf6aa4d7dadd4e8826a9b6d264..0141e524fc7a398330a701b8e31f89aa37688c42 100644
--- a/src/rt/debug/rt.h
+++ b/src/rt/debug/rt.h
@@ -51,8 +51,7 @@ rt_compute_stellar_emission_rate(struct spart* restrict sp, double time,
   sp->rt_data.debug_emission_rate_set += 1;
 
   /* rt_set_stellar_emission_rate(sp, star_age_begin_of_step, star_age,
-   * rt_props, */
-  /*                              phys_const, internal_units); */
+   *                              rt_props, phys_const, internal_units); */
 }
 
 /**
@@ -66,10 +65,10 @@ __attribute__((always_inline)) INLINE static void rt_init_part(
     struct part* restrict p) {}
 
 /**
- * @brief Reset of the RT hydro particle data not related to the density.
+ * @brief Reset the RT hydro particle data not related to the hydro density.
  * Note: during initalisation (space_init), rt_reset_part and rt_init_part
- * are both called individually. Also an extra call to rt_reset_part is made
- * in space_convert_rt_quantities_after_zeroth_step().
+ * are both called individually. To reset RT data needed in each RT sub-cycle,
+ * use rt_reset_part_each_subcycle().
  *
  * @param p the particle to work on
  * @param cosmo Cosmology.
@@ -80,57 +79,35 @@ __attribute__((always_inline)) INLINE static void rt_reset_part(
   /* reset this here as well as in the rt_debugging_checks_end_of_step()
    * routine to test task dependencies are done right */
   p->rt_data.debug_iact_stars_inject = 0;
-
-  p->rt_data.debug_calls_iact_gradient_interaction = 0;
-  p->rt_data.debug_calls_iact_transport_interaction = 0;
-
+  p->rt_data.debug_nsubcycles = 0;
   p->rt_data.debug_kicked = 0;
-  p->rt_data.debug_injection_done = 0;
-  p->rt_data.debug_gradients_done = 0;
-  p->rt_data.debug_transport_done = 0;
-  p->rt_data.debug_thermochem_done = 0;
 }
 
 /**
- * @brief First initialisation of the RT hydro particle data.
+ * @brief Reset RT particle data which needs to be reset each sub-cycle.
  *
- * @param p particle to work on
- * @param cosmo #cosmology data structure.
- * @param rt_props RT properties struct
+ * @param p the particle to work on
  */
-__attribute__((always_inline)) INLINE static void rt_first_init_part(
-    struct part* restrict p, const struct cosmology* cosmo,
-    const struct rt_props* restrict rt_props) {
+__attribute__((always_inline)) INLINE static void rt_reset_part_each_subcycle(
+    struct part* restrict p) {
 
-  rt_init_part(p);
-  rt_reset_part(p, cosmo);
-  p->rt_data.debug_radiation_absorbed_tot = 0ULL;
+  rt_debugging_reset_each_subcycle(p);
 }
 
 /**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the particle density and time step to be known.
+ * @brief First initialisation of the RT hydro particle data.
  *
  * @param p particle to work on
- * @param rt_props RT properties struct
  * @param cosmo #cosmology data structure.
+ * @param rt_props RT properties struct
  */
-__attribute__((always_inline)) INLINE static void
-rt_init_part_after_zeroth_step(struct part* restrict p,
-                               const struct rt_props* rt_props,
-                               const struct cosmology* restrict cosmo) {
+__attribute__((always_inline)) INLINE static void rt_first_init_part(
+    struct part* restrict p, const struct cosmology* cosmo,
+    const struct rt_props* restrict rt_props) {
 
-  /* If we're running with debugging checks on, reset debugging
-   * counters and flags in particular after the zeroth step so
-   * that the checks work as intended. */
   rt_init_part(p);
   rt_reset_part(p, cosmo);
-  /* Since the inject_prep has been moved to the density loop, the
-   * initialization at startup is messing with the total counters for stars
-   * because the density is called, but not the force-and-kick tasks. So reset
-   * the total counters here as well so that they will match the star counters.
-   */
+  rt_reset_part_each_subcycle(p);
   p->rt_data.debug_radiation_absorbed_tot = 0ULL;
 }
 
@@ -154,8 +131,7 @@ __attribute__((always_inline)) INLINE static void rt_init_spart(
 /**
  * @brief Reset of the RT star particle data not related to the density.
  * Note: during initalisation (space_init), rt_reset_spart and rt_init_spart
- * are both called individually. Also an extra call to rt_reset_spart is made
- * in space_convert_rt_quantities_after_zeroth_step().
+ * are both called individually.
  *
  * @param sp star particle to work on
  */
@@ -175,37 +151,6 @@ __attribute__((always_inline)) INLINE static void rt_first_init_spart(
   sp->rt_data.debug_radiation_emitted_tot = 0ULL;
 }
 
-/**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the star density and time step to be known.
- * @param sp star particle to work on
- * @param time current system time
- * @param star_age age of the star *at the end of the step*
- * @param dt star time step
- * @param rt_props RT properties struct
- * @param phys_const physical constants struct
- * @param internal_units struct holding internal units
- */
-__attribute__((always_inline)) INLINE static void
-rt_init_star_after_zeroth_step(struct spart* restrict sp, double time,
-                               double star_age, double dt,
-                               const struct rt_props* rt_props,
-                               const struct phys_const* phys_const,
-                               const struct unit_system* internal_units) {
-
-  /* If we're running with debugging checks on, reset debugging
-   * counters and flags in particular after the zeroth step so
-   * that the checks work as intended. */
-  rt_init_spart(sp);
-  rt_reset_spart(sp);
-  /* Since the inject_prep has been moved to the density loop, the
-   * initialization at startup is messing with the total counters because
-   * the density is called, but not the force-and-kick tasks. So reset
-   * the total counters here as well. */
-  sp->rt_data.debug_radiation_emitted_tot = 0ULL;
-}
-
 /**
  * @brief Split the RT data of a particle into n pieces
  *
@@ -213,7 +158,9 @@ rt_init_star_after_zeroth_step(struct spart* restrict sp, double time,
  * @param n The number of pieces to split into.
  */
 __attribute__((always_inline)) INLINE static void rt_split_part(struct part* p,
-                                                                double n) {}
+                                                                double n) {
+  error("RT can't run with split particles for now.");
+}
 
 /**
  * @brief Exception handle a hydro part not having any neighbours in ghost task
@@ -316,9 +263,8 @@ __attribute__((always_inline)) INLINE static double rt_part_dt(
 __attribute__((always_inline)) INLINE static void rt_finalise_injection(
     struct part* restrict p, struct rt_props* props) {
 
-  if (p->rt_data.debug_kicked != 1)
-    error("called rt_ghost1 when particle %lld is unkicked (count=%d)", p->id,
-          p->rt_data.debug_kicked);
+  rt_debug_sequence_check(p, 1, "rt_ghost1/rt_finalise_injection");
+
   p->rt_data.debug_injection_done += 1;
 }
 
@@ -331,15 +277,7 @@ __attribute__((always_inline)) INLINE static void rt_finalise_injection(
 __attribute__((always_inline)) INLINE static void rt_end_gradient(
     struct part* restrict p, const struct cosmology* cosmo) {
 
-  if (p->rt_data.debug_kicked != 1)
-    error("called finalise gradient when particle %lld is unkicked (count=%d)",
-          p->id, p->rt_data.debug_kicked);
-
-  if (p->rt_data.debug_injection_done != 1)
-    error(
-        "Called finalise gradient on particle %lld"
-        "where injection_done count = %d",
-        p->id, p->rt_data.debug_injection_done);
+  rt_debug_sequence_check(p, 2, __func__);
 
   if (p->rt_data.debug_calls_iact_gradient_interaction == 0)
     message(
@@ -361,21 +299,7 @@ __attribute__((always_inline)) INLINE static void rt_finalise_transport(
     struct part* restrict p, const double dt,
     const struct cosmology* restrict cosmo) {
 
-  if (p->rt_data.debug_kicked != 1)
-    error("called finalise transport when particle %lld is unkicked (count=%d)",
-          p->id, p->rt_data.debug_kicked);
-
-  if (p->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do finalise_transport on particle %lld when "
-        "injection_done count is %d",
-        p->id, p->rt_data.debug_injection_done);
-
-  if (p->rt_data.debug_gradients_done != 1)
-    error(
-        "Trying to do finalise_transport on particle %lld when "
-        "gradients_done count is %d",
-        p->id, p->rt_data.debug_gradients_done);
+  rt_debug_sequence_check(p, 3, __func__);
 
   if (p->rt_data.debug_calls_iact_transport_interaction == 0)
     message(
@@ -407,28 +331,15 @@ __attribute__((always_inline)) INLINE static void rt_tchem(
     const struct phys_const* restrict phys_const,
     const struct unit_system* restrict us, const double dt) {
 
-  if (p->rt_data.debug_kicked != 1)
-    error(
-        "Part %lld trying to do thermochemistry on unkicked particle "
-        "(count=%d)",
-        p->id, p->rt_data.debug_kicked);
-  if (p->rt_data.debug_injection_done != 1)
-    error("Part %lld trying to do thermochemistry when injection_done != 1: %d",
-          p->id, p->rt_data.debug_injection_done);
-  if (p->rt_data.debug_gradients_done != 1)
-    error("Part %lld trying to do thermochemistry when gradients_done != 1: %d",
-          p->id, p->rt_data.debug_gradients_done);
-  if (p->rt_data.debug_transport_done != 1)
-    error("Part %lld trying to do thermochemistry when transport_done != 1: %d",
-          p->id, p->rt_data.debug_transport_done);
-
+  rt_debug_sequence_check(p, 4, __func__);
   p->rt_data.debug_thermochem_done += 1;
 
   /* rt_do_thermochemistry(p); */
 }
 
 /**
- * @brief Extra operations done during the kick.
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
  *
  * @param p Particle to act upon.
  * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
@@ -447,6 +358,8 @@ __attribute__((always_inline)) INLINE static void rt_kick_extra(
   /* Don't account for timestep_sync backward kicks */
   if (dt_therm >= 0.f && dt_grav >= 0.f && dt_hydro >= 0.f &&
       dt_kick_corr >= 0.f) {
+
+    rt_debug_sequence_check(p, 0, __func__);
     p->rt_data.debug_kicked += 1;
   }
 }
diff --git a/src/rt/debug/rt_debugging.h b/src/rt/debug/rt_debugging.h
index 83e670d3f22f8a018e76c654751f95a701c55a85..c1f42667c142dc5ad3078da441a7af6708ec777a 100644
--- a/src/rt/debug/rt_debugging.h
+++ b/src/rt/debug/rt_debugging.h
@@ -19,7 +19,9 @@
 #ifndef SWIFT_RT_DEBUGGING_DEBUG_H
 #define SWIFT_RT_DEBUGGING_DEBUG_H
 
+#include "active.h"
 #include "rt_properties.h"
+#include "timeline.h"
 
 /**
  * @file src/rt/debug/rt_debugging.h
@@ -27,6 +29,100 @@
  * extra debugging functions.
  */
 
+/**
+ * @brief This resets particle carried quantities after each subcycling
+ * step such that the internal checks are still consistent.
+ * @param p the particle to work on
+ */
+__attribute__((always_inline)) INLINE static void rt_debugging_count_subcycle(
+    struct part *restrict p) {
+  p->rt_data.debug_nsubcycles += 1;
+}
+
+/**
+ * @brief Check that the particle completed the correct number of subcycles.
+ * This is checked in every rt_reset_part, before the subcycling count is reset.
+ * @param p the particle to work on
+ * @param rt_props RT properties struct
+ */
+__attribute__((always_inline)) INLINE static void
+rt_debugging_check_nr_subcycles(struct part *restrict p,
+                                const struct rt_props *rt_props) {
+
+  /* TODO: this check may fail when running with limiter/sync. */
+
+  /* NOTE: we need to do this check somewhere in the hydro tasks.
+   * (1) it needs to be done when all tasks are active and before the
+   * particle hydro time step is changed.
+   * (2) If you do it during RT tasks, it won't properly check how
+   * many sub-cycles you did during a single hydro task.
+   * (3) You can't do it during the timestep task, since between
+   * the hydro and the timestep we already do an RT step. */
+
+  /* skip initialization */
+  if (p->time_bin == 0) return;
+  if (p->rt_time_data.time_bin == 0)
+    error("Got part %lld with RT time bin 0", p->id);
+
+  timebin_t bindiff = p->time_bin - p->rt_time_data.time_bin;
+
+  if (rt_props->debug_max_nr_subcycles <= 1) {
+    /* Running without subcycling. */
+    if (bindiff != 0)
+      error("Running without subcycling but got bindiff=%d for part=%lld",
+            bindiff, p->id);
+    if (p->rt_data.debug_nsubcycles != 1)
+      error("Running without subcycling but got part=%lld subcycle count=%d",
+            p->id, p->rt_data.debug_nsubcycles);
+    return;
+  }
+
+  /* TODO: this assumes that max_nr_subcycles is not an upper limit,
+   * but a fixed number of sub-cycles */
+  timebin_t bindiff_expect = 0;
+
+  while (!(rt_props->debug_max_nr_subcycles & (1 << bindiff_expect)) &&
+         bindiff_expect != num_time_bins)
+    ++bindiff_expect;
+
+  if (bindiff_expect == num_time_bins)
+    error(
+        "Couldn't determine expected time bin difference. Max nr subcycles %d "
+        "bindiff %d",
+        rt_props->debug_max_nr_subcycles, bindiff);
+
+  if (bindiff != bindiff_expect)
+    error("Particle %lld Got bindiff=%d expect=%d; timebins=%d rt=%d", p->id,
+          bindiff, bindiff_expect, p->time_bin, p->rt_time_data.time_bin);
+
+  int subcycles_expect = (1 << bindiff);
+  if (p->rt_data.debug_nsubcycles != subcycles_expect)
+
+    if (p->rt_data.debug_nsubcycles != rt_props->debug_max_nr_subcycles)
+      error(
+          "Particle %lld didn't do the expected amount of subcycles: Expected "
+          "%d, done %d; time bins %d RT: %d",
+          p->id, subcycles_expect, p->rt_data.debug_nsubcycles, p->time_bin,
+          p->rt_time_data.time_bin);
+}
+
+/**
+ * @brief This resets particle carried quantities after each subcycling
+ * step such that the internal checks are still consistent.
+ * @param p the particle to work on
+ */
+__attribute__((always_inline)) INLINE static void
+rt_debugging_reset_each_subcycle(struct part *restrict p) {
+
+  p->rt_data.debug_calls_iact_gradient_interaction = 0;
+  p->rt_data.debug_calls_iact_transport_interaction = 0;
+
+  p->rt_data.debug_injection_done = 0;
+  p->rt_data.debug_gradients_done = 0;
+  p->rt_data.debug_transport_done = 0;
+  p->rt_data.debug_thermochem_done = 0;
+}
+
 /**
  * @brief Debugging checks loop over all star particles after each time step
  */
@@ -73,6 +169,7 @@ static void rt_debugging_end_of_step_hydro_mapper(void *restrict map_data,
     struct part *restrict p = &parts[k];
     absorption_sum_this_step += p->rt_data.debug_iact_stars_inject;
     absorption_sum_tot += p->rt_data.debug_radiation_absorbed_tot;
+
     /* Reset all values here in case particles won't be active next step */
     p->rt_data.debug_iact_stars_inject = 0;
   }
@@ -95,11 +192,6 @@ rt_debugging_checks_end_of_step(struct engine *e, int verbose) {
 
   struct space *s = e->s;
   if (!(e->policy & engine_policy_rt)) return;
-#ifdef WITH_MPI
-  /* Since we aren't sending data back, none of these checks will
-   * pass a run over MPI. */
-  return;
-#endif
 
   const ticks tic = getticks();
 
@@ -123,7 +215,15 @@ rt_debugging_checks_end_of_step(struct engine *e, int verbose) {
                    s->sparts, s->nr_sparts, sizeof(struct spart),
                    threadpool_auto_chunk_size, /*extra_data=*/e);
 
+#ifdef WITH_MPI
+  /* Since we aren't sending data back, none of these checks will
+   * pass a run over MPI. Make sure you run the threadpool functions
+   * first though so certain variables can get reset properly. */
+  return;
+#endif
+
   /* Have we accidentally invented or deleted some radiation somewhere? */
+
   if ((e->rt_props->debug_radiation_emitted_this_step !=
        e->rt_props->debug_radiation_absorbed_this_step) ||
       (e->rt_props->debug_radiation_emitted_tot !=
@@ -142,4 +242,77 @@ rt_debugging_checks_end_of_step(struct engine *e, int verbose) {
             clocks_getunit());
 }
 
+/**
+ * @brief Perform a series of consistency and sanity checks.
+ *
+ * @param p particle to check
+ * @param loc location where this is called from. This determines which checks
+ * will be done:
+ *
+ * 0: during kicks/after drifts.
+ * 1: during rt_ghost1/finalise_injection / after kicks.
+ * 2: during gradients / after injection.
+ * 3: during transport / after gradients.
+ * 4: during thermochem / after transport.
+ * 5: after thermochem.
+ *
+ * @param function_name: Function name (or message) you want printed on error.
+ */
+__attribute__((always_inline)) INLINE static void rt_debug_sequence_check(
+    struct part *restrict p, int loc, const char *function_name) {
+
+  /* Note: Checking whether a particle has been drifted at all is not
+   * compatible with subcycling. There is no reliable point where to
+   * reset the counters and have sensible results. */
+
+  if (loc > 0) {
+    /* Are kicks done? */
+    if (p->rt_data.debug_nsubcycles == 0) {
+      if (p->rt_data.debug_kicked != 1)
+        error(
+            "called %s on particle %lld with wrong kick count=%d (expected "
+            "1) cycle=%d",
+            function_name, p->id, p->rt_data.debug_kicked,
+            p->rt_data.debug_nsubcycles);
+    } else if (p->rt_data.debug_nsubcycles > 0) {
+      if (p->rt_data.debug_kicked != 2)
+        error(
+            "called %s on particle %lld with wrong kick count=%d (expected 2) "
+            "cycle=%d",
+            function_name, p->id, p->rt_data.debug_kicked,
+            p->rt_data.debug_nsubcycles);
+    } else {
+      error("Got negative subcycle???");
+    }
+  }
+
+  if (loc > 1) {
+    /* is injection done? */
+    if (p->rt_data.debug_injection_done != 1)
+      error("called %s on part %lld when finalise injection count is %d ID",
+            function_name, p->id, p->rt_data.debug_injection_done);
+  }
+
+  if (loc > 2) {
+    /* are gradients done? */
+    if (p->rt_data.debug_gradients_done != 1)
+      error("called %s on part %lld when gradients_done count is %d",
+            function_name, p->id, p->rt_data.debug_gradients_done);
+  }
+
+  if (loc > 3) {
+    /* is transport done? */
+    if (p->rt_data.debug_transport_done != 1)
+      error("called %s on part %lld when transport_done != 1: %d",
+            function_name, p->id, p->rt_data.debug_transport_done);
+  }
+
+  if (loc > 4) {
+    /* is thermochemistry done? */
+    if (p->rt_data.debug_thermochem_done != 1)
+      error("called %s on part %lld with thermochem_done count=%d",
+            function_name, p->id, p->rt_data.debug_thermochem_done);
+  }
+}
+
 #endif /* SWIFT_RT_DEBUGGING_DEBUG_H */
diff --git a/src/rt/debug/rt_gradients.h b/src/rt/debug/rt_gradients.h
index f79affc17d586ea9ff7bee222038e7366e7d30f2..3d2dfc8b0cd238e100872df5d1827e928cd3034c 100644
--- a/src/rt/debug/rt_gradients.h
+++ b/src/rt/debug/rt_gradients.h
@@ -19,6 +19,8 @@
 #ifndef SWIFT_RT_GRADIENTS_DEBUG_H
 #define SWIFT_RT_GRADIENTS_DEBUG_H
 
+#include "rt_debugging.h"
+
 /**
  * @file src/rt/debug/rt_gradients.h
  * @brief Main header file for the debug radiative transfer scheme gradients
@@ -38,32 +40,10 @@ __attribute__((always_inline)) INLINE static void rt_gradients_collect(
     float r2, const float dx[3], float hi, float hj, struct part *restrict pi,
     struct part *restrict pj) {
 
-  if (pi->rt_data.debug_kicked != 1)
-    error(
-        "Trying to do symmetric iact gradient unkicked particle %lld "
-        "(count=%d)",
-        pi->id, pi->rt_data.debug_kicked);
-
-  if (pi->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do symmetric iact gradient when finalise injection count is "
-        "%d ID %lld",
-        pi->rt_data.debug_injection_done, pi->id);
-
-  if (pj->rt_data.debug_kicked != 1)
-    error(
-        "Trying to do symmetric iact gradient unkicked particle %lld "
-        "(count=%d)",
-        pj->id, pj->rt_data.debug_kicked);
-
-  if (pj->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do symmetric iact gradient when finalise injection count is "
-        "%d ID %lld",
-        pj->rt_data.debug_injection_done, pj->id);
+  rt_debug_sequence_check(pi, 2, __func__);
+  rt_debug_sequence_check(pj, 2, __func__);
 
   pi->rt_data.debug_calls_iact_gradient_interaction += 1;
-
   pj->rt_data.debug_calls_iact_gradient_interaction += 1;
 }
 
@@ -81,18 +61,7 @@ __attribute__((always_inline)) INLINE static void rt_gradients_nonsym_collect(
     float r2, const float dx[3], float hi, float hj, struct part *restrict pi,
     struct part *restrict pj) {
 
-  if (pi->rt_data.debug_kicked != 1)
-    error(
-        "Trying to do nonsym iact gradient on unkicked particle %lld "
-        "(count=%d)",
-        pi->id, pi->rt_data.debug_kicked);
-
-  if (pi->rt_data.debug_injection_done != 1)
-    error(
-        "Trying to do nonsym iact gradients when finalise injection count is "
-        "%d ID %lld",
-        pi->rt_data.debug_injection_done, pi->id);
-
+  rt_debug_sequence_check(pi, 2, __func__);
   pi->rt_data.debug_calls_iact_gradient_interaction += 1;
 }
 
diff --git a/src/rt/debug/rt_iact.h b/src/rt/debug/rt_iact.h
index 6c6276c85d27fee6b048b757935fb610fb6afe56..2eb590dd43281affb0c3aece562a7d5f71948376 100644
--- a/src/rt/debug/rt_iact.h
+++ b/src/rt/debug/rt_iact.h
@@ -19,6 +19,7 @@
 #ifndef SWIFT_RT_IACT_DEBUG_H
 #define SWIFT_RT_IACT_DEBUG_H
 
+#include "rt_debugging.h"
 #include "rt_gradients.h"
 
 /**
@@ -73,6 +74,8 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_inject(
    * have nothing to do here. */
   if (si->density.wcount == 0.f) return;
 
+  /* Do some checks and increase neighbour counts
+   * before other potential early exits */
   if (si->rt_data.debug_iact_hydro_inject_prep == 0)
     error(
         "Injecting energy from star that wasn't called during injection prep");
@@ -107,42 +110,12 @@ __attribute__((always_inline)) INLINE static void runner_iact_rt_flux_common(
     float r2, const float *dx, float hi, float hj, struct part *restrict pi,
     struct part *restrict pj, float a, float H, int mode) {
 
-  if (pi->rt_data.debug_kicked != 1)
-    error("Trying to iact transport with unkicked particle %lld (count=%d)",
-          pi->id, pi->rt_data.debug_kicked);
-
-  if (pi->rt_data.debug_injection_done != 1)
-    error(
-        "Part %lld trying to do iact transport when "
-        "injection_done count is %d",
-        pi->id, pi->rt_data.debug_injection_done);
-
-  if (pi->rt_data.debug_gradients_done != 1)
-    error(
-        "Part %lld trying to do iact transport when "
-        "gradients_done count is %d",
-        pi->id, pi->rt_data.debug_gradients_done);
-
+  const char *func_name = (mode == 1) ? "sym flux iact" : "nonsym flux iact";
+  rt_debug_sequence_check(pi, 3, func_name);
   pi->rt_data.debug_calls_iact_transport_interaction += 1;
 
   if (mode == 1) {
-
-    if (pj->rt_data.debug_kicked != 1)
-      error("Trying to iact transport with unkicked particle %lld (count=%d)",
-            pj->id, pj->rt_data.debug_kicked);
-
-    if (pj->rt_data.debug_injection_done != 1)
-      error(
-          "Part %lld Trying to do iact transport when "
-          "injection_done count is %d",
-          pj->id, pj->rt_data.debug_injection_done);
-
-    if (pj->rt_data.debug_gradients_done != 1)
-      error(
-          "Part %lld Trying to do iact transport when "
-          "gradients_done count is %d",
-          pj->id, pj->rt_data.debug_gradients_done);
-
+    rt_debug_sequence_check(pj, 3, func_name);
     pj->rt_data.debug_calls_iact_transport_interaction += 1;
   }
 }
diff --git a/src/rt/debug/rt_io.h b/src/rt/debug/rt_io.h
index 8ac58f13fb7747ae9edb04dddce66f701b80800b..779724d2576dd9a1d26ec25a3e78005894d584f4 100644
--- a/src/rt/debug/rt_io.h
+++ b/src/rt/debug/rt_io.h
@@ -95,8 +95,11 @@ INLINE static int rt_write_particles(const struct part* parts,
       "RTDebugRadAbsorbedTot", ULONGLONG, 1, UNIT_CONV_NO_UNITS, 0, parts,
       rt_data.debug_radiation_absorbed_tot,
       "Radiation absorbed by this part during its lifetime");
+  list[7] = io_make_output_field("RTDebugSubcycles", INT, 1, UNIT_CONV_NO_UNITS,
+                                 0, parts, rt_data.debug_nsubcycles,
+                                 "How many times this part was subcycled");
 
-  return 7;
+  return 8;
 }
 
 /**
diff --git a/src/rt/debug/rt_properties.h b/src/rt/debug/rt_properties.h
index eb213e117edcac7139096f287487e84bde5274c9..215639886413f8e39cff3d4b78b583bfaa4f8cb1 100644
--- a/src/rt/debug/rt_properties.h
+++ b/src/rt/debug/rt_properties.h
@@ -47,6 +47,9 @@ struct rt_props {
   /* total radiation absorbed by gas. This is not really a property,
    * but a placeholder to sum up a global variable */
   unsigned long long debug_radiation_absorbed_tot;
+
+  /* Max number of subcycles per hydro step */
+  int debug_max_nr_subcycles;
 };
 
 /**
@@ -83,13 +86,10 @@ __attribute__((always_inline)) INLINE static void rt_props_init(
   rtp->debug_radiation_absorbed_tot = 0ULL;
   rtp->debug_radiation_absorbed_this_step = 0ULL;
 
-  /* After initialisation, print params to screen */
-  rt_props_print(rtp);
-
-  /* Print a final message. */
-  if (engine_rank == 0) {
-    message("Radiative transfer initialized");
-  }
+  /* Don't make it an optional parameter here so we crash
+   * if I forgot to provide it */
+  rtp->debug_max_nr_subcycles =
+      parser_get_param_int(params, "TimeIntegration:max_nr_rt_subcycles");
 }
 
 /**
diff --git a/src/rt/debug/rt_struct.h b/src/rt/debug/rt_struct.h
index 2ab50cc7513979b34a901dcf0ccfb725a295c920..bb3742f98c0256b76fa8b33231abaa16fce1d72d 100644
--- a/src/rt/debug/rt_struct.h
+++ b/src/rt/debug/rt_struct.h
@@ -19,6 +19,8 @@
 #ifndef SWIFT_RT_STRUCT_DEBUG_H
 #define SWIFT_RT_STRUCT_DEBUG_H
 
+#include "timeline.h"
+
 /**
  * @file src/rt/debug/rt_struct.h
  * @brief Main header file for the debug radiative transfer struct.
@@ -61,6 +63,11 @@ struct rt_part_data {
 
   /*! thermochemistry done? */
   int debug_thermochem_done;
+
+  /* Subcycling flags */
+
+  /*! Current subcycle wrt (last) hydro step */
+  int debug_nsubcycles;
 };
 
 /* Additional RT data in star particle struct */
diff --git a/src/rt/none/rt.h b/src/rt/none/rt.h
index 57e2b77302b9a83abefed8a96b126dbe2c88c217..de9b3a8aa2b20c65297dd66e695f893dc0844ad2 100644
--- a/src/rt/none/rt.h
+++ b/src/rt/none/rt.h
@@ -61,15 +61,25 @@ __attribute__((always_inline)) INLINE static void rt_init_part(
     struct part* restrict p) {}
 
 /**
- * @brief Reset of the RT hydro particle data not related to the density.
+ * @brief Reset the RT hydro particle data not related to the hydro density.
  * Note: during initalisation (space_init), rt_reset_part and rt_init_part
- * are both called individually.
+ * are both called individually. To reset RT data needed in each RT sub-cycle,
+ * use rt_reset_part_each_subcycle().
+ *
  * @param p particle to work on
  * @param cosmo Cosmology.
  */
 __attribute__((always_inline)) INLINE static void rt_reset_part(
     struct part* restrict p, const struct cosmology* cosmo) {}
 
+/**
+ * @brief Reset RT particle data which needs to be reset each sub-cycle.
+ *
+ * @param p the particle to work on
+ */
+__attribute__((always_inline)) INLINE static void rt_reset_part_each_subcycle(
+    struct part* restrict p) {}
+
 /**
  * @brief First initialisation of the RT hydro particle data.
  *
@@ -81,20 +91,6 @@ __attribute__((always_inline)) INLINE static void rt_first_init_part(
     struct part* restrict p, const struct cosmology* cosmo,
     const struct rt_props* restrict rt_props) {}
 
-/**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the particle density and time step to be known.
- *
- * @param p particle to work on
- * @param rt_props RT properties struct
- * @param cosmo #cosmology data structure.
- */
-__attribute__((always_inline)) INLINE static void
-rt_init_part_after_zeroth_step(struct part* restrict p,
-                               const struct rt_props* rt_props,
-                               const struct cosmology* restrict cosmo) {}
-
 /**
  * @brief Initialisation of the RT density loop related star particle data.
  * Note: during initalisation (space_init), rt_reset_spart and rt_init_spart
@@ -123,25 +119,6 @@ __attribute__((always_inline)) INLINE static void rt_reset_spart(
 __attribute__((always_inline)) INLINE static void rt_first_init_spart(
     struct spart* restrict sp) {}
 
-/**
- * @brief Initialises particle quantities that can't be set
- * otherwise before the zeroth step is finished. E.g. because
- * they require the star density and time step to be known.
- * @param sp star particle to work on
- * @param time current system time
- * @param star_age age of the star *at the end of the step*
- * @param dt star time step
- * @param rt_props RT properties struct
- * @param phys_const physical constants struct
- * @param internal_units struct holding internal units
- */
-__attribute__((always_inline)) INLINE static void
-rt_init_star_after_zeroth_step(struct spart* restrict sp, double time,
-                               double star_age, double dt,
-                               const struct rt_props* rt_props,
-                               const struct phys_const* phys_const,
-                               const struct unit_system* internal_units) {}
-
 /**
  * @brief Split the RT data of a particle into n pieces
  *
diff --git a/src/rt/none/rt_properties.h b/src/rt/none/rt_properties.h
index 251b142e3bf6217b152b5e22c34f5d4b082ac539..f769507f6871d6ddf8002dd2a909049ef570e211 100644
--- a/src/rt/none/rt_properties.h
+++ b/src/rt/none/rt_properties.h
@@ -57,11 +57,7 @@ __attribute__((always_inline)) INLINE static void rt_props_print(
 __attribute__((always_inline)) INLINE static void rt_props_init(
     struct rt_props* rtp, const struct phys_const* phys_const,
     const struct unit_system* us, struct swift_params* params,
-    struct cosmology* cosmo) {
-
-  /* After initialisation, print params to screen */
-  rt_props_print(rtp);
-}
+    struct cosmology* cosmo) {}
 
 /**
  * @brief Write an RT properties struct to the given FILE as a
diff --git a/src/rt_struct.h b/src/rt_struct.h
index f6dfd5595327dc3fd2caa9e014a74434df0a37b9..6474f2efa731366766d41594d867b845ef016b90 100644
--- a/src/rt_struct.h
+++ b/src/rt_struct.h
@@ -25,6 +25,8 @@
  */
 
 /* Config parameters. */
+#include "timeline.h"
+
 #include <config.h>
 
 /* Import the right RT struct definition */
@@ -40,4 +42,29 @@
 #error "Invalid choice of radiation scheme"
 #endif
 
+/* Define a struct to contain all RT sub-cycling related
+ * timestepping variables here. These variables need to be
+ * identical for every scheme and users should never touch
+ * them anyway, so hide them here. */
+
+#if defined(RT_NONE)
+
+/*! empty placeholder for RT timestepping data. */
+struct rt_timestepping_data {
+  union {
+    /*! Time-bin this particle uses for RT interactions */
+    timebin_t time_bin;
+  };
+};
+
+#else
+
+/*! data relevant to the sub-cycle timestepping of parts. */
+struct rt_timestepping_data {
+
+  /*! Time-bin this particle uses for RT interactions */
+  timebin_t time_bin;
+};
+#endif
+
 #endif /* SWIFT_RT_STRUCT_H */
diff --git a/src/runner.h b/src/runner.h
index fa71fd817a9251984128a3e134b0bdc414b2f59d..4ccd3cec4ac7e6ef1edffb0e81ec16da54d15145 100644
--- a/src/runner.h
+++ b/src/runner.h
@@ -100,7 +100,7 @@ void runner_do_black_holes_swallow_ghost(struct runner *r, struct cell *c,
                                          int timer);
 void runner_do_init_grav(struct runner *r, struct cell *c, int timer);
 void runner_do_hydro_sort(struct runner *r, struct cell *c, int flag,
-                          int cleanup, int clock);
+                          int cleanup, int rt_requests_sort, int clock);
 void runner_do_stars_sort(struct runner *r, struct cell *c, int flag,
                           int cleanup, int clock);
 void runner_do_all_hydro_sort(struct runner *r, struct cell *c);
@@ -153,6 +153,10 @@ void runner_do_pack_limiter(struct runner *r, struct cell *c, void **buffer,
 void runner_do_unpack_limiter(struct runner *r, struct cell *c, void *buffer,
                               const int timer);
 void runner_do_neutrino_weighting(struct runner *r, struct cell *c, int timer);
+void runner_do_rt_advance_cell_time(struct runner *r, struct cell *c,
+                                    int timer);
+void runner_do_collect_rt_times(struct runner *r, struct cell *c,
+                                const int timer);
 void *runner_main(void *data);
 
 ticks runner_get_active_time(const struct runner *restrict r);
diff --git a/src/runner_doiact_functions_hydro.h b/src/runner_doiact_functions_hydro.h
index b13d3ebc153cd70170d9dc5108865bdaf11ffe70..9c3f15a4175496aac9ca77107446c1e412323a82 100644
--- a/src/runner_doiact_functions_hydro.h
+++ b/src/runner_doiact_functions_hydro.h
@@ -49,7 +49,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
   TIMER_TIC;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(ci, e) && !cell_is_active_hydro(cj, e)) return;
+  if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
 
   const int count_i = ci->hydro.count;
   const int count_j = cj->hydro.count;
@@ -79,7 +79,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
 
-    const int pi_active = part_is_active(pi, e);
+    const int pi_active = PART_IS_ACTIVE(pi, e);
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - (cj->loc[0] + shift[0])),
@@ -97,7 +97,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
 
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
-      const int pj_active = part_is_active(pj, e);
+      const int pj_active = PART_IS_ACTIVE(pj, e);
 
       /* Compute the pairwise distance. */
       const float pjx[3] = {(float)(pj->x[0] - cj->loc[0]),
@@ -106,7 +106,7 @@ void DOPAIR1_NAIVE(struct runner *r, struct cell *restrict ci,
       float dx[3] = {pix[0] - pjx[0], pix[1] - pjx[1], pix[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
       if (pi->ti_drift != e->ti_current)
         error("Particle pi not drifted to current time");
@@ -182,7 +182,7 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
   TIMER_TIC;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(ci, e) && !cell_is_active_hydro(cj, e)) return;
+  if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
 
   const int count_i = ci->hydro.count;
   const int count_j = cj->hydro.count;
@@ -212,7 +212,7 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
 
-    const int pi_active = part_is_active(pi, e);
+    const int pi_active = PART_IS_ACTIVE(pi, e);
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - (cj->loc[0] + shift[0])),
@@ -228,7 +228,7 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
       /* Skip inhibited particles. */
       if (part_is_inhibited(pj, e)) continue;
 
-      const int pj_active = part_is_active(pj, e);
+      const int pj_active = PART_IS_ACTIVE(pj, e);
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
 
@@ -239,7 +239,7 @@ void DOPAIR2_NAIVE(struct runner *r, struct cell *restrict ci,
       float dx[3] = {pix[0] - pjx[0], pix[1] - pjx[1], pix[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
       if (pi->ti_drift != e->ti_current)
         error("Particle pi not drifted to current time");
@@ -329,7 +329,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
   TIMER_TIC;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(c, e)) return;
+  if (!CELL_IS_ACTIVE(c, e)) return;
 
   /* Cosmological terms and physical constants */
   const float a = cosmo->a;
@@ -348,7 +348,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
 
-    const int pi_active = part_is_active(pi, e);
+    const int pi_active = PART_IS_ACTIVE(pi, e);
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - c->loc[0]),
@@ -366,7 +366,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
 
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
-      const int pj_active = part_is_active(pj, e);
+      const int pj_active = PART_IS_ACTIVE(pj, e);
 
       /* Compute the pairwise distance. */
       const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
@@ -378,7 +378,7 @@ void DOSELF1_NAIVE(struct runner *r, struct cell *restrict c) {
       const int doi = pi_active && (r2 < hig2);
       const int doj = pj_active && (r2 < hjg2);
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
       if (pi->ti_drift != e->ti_current)
         error("Particle pi not drifted to current time");
@@ -465,7 +465,7 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
   TIMER_TIC;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(c, e)) return;
+  if (!CELL_IS_ACTIVE(c, e)) return;
 
   /* Cosmological terms and physical constants */
   const float a = cosmo->a;
@@ -484,7 +484,7 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
     /* Skip inhibited particles. */
     if (part_is_inhibited(pi, e)) continue;
 
-    const int pi_active = part_is_active(pi, e);
+    const int pi_active = PART_IS_ACTIVE(pi, e);
     const float hi = pi->h;
     const float hig2 = hi * hi * kernel_gamma2;
     const float pix[3] = {(float)(pi->x[0] - c->loc[0]),
@@ -502,7 +502,7 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
 
       const float hj = pj->h;
       const float hjg2 = hj * hj * kernel_gamma2;
-      const int pj_active = part_is_active(pj, e);
+      const int pj_active = PART_IS_ACTIVE(pj, e);
 
       /* Compute the pairwise distance. */
       const float pjx[3] = {(float)(pj->x[0] - c->loc[0]),
@@ -514,7 +514,7 @@ void DOSELF2_NAIVE(struct runner *r, struct cell *restrict c) {
       const int doi = pi_active && ((r2 < hig2) || (r2 < hjg2));
       const int doj = pj_active && ((r2 < hig2) || (r2 < hjg2));
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
       if (pi->ti_drift != e->ti_current)
         error("Particle pi not drifted to current time");
@@ -649,7 +649,7 @@ void DOPAIR_SUBSET_NAIVE(struct runner *r, struct cell *restrict ci,
         r2 += dx[k] * dx[k];
       }
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
       if (pi->ti_drift != e->ti_current)
         error("Particle pi not drifted to current time");
@@ -757,7 +757,7 @@ void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
                        (float)(piz - pjz)};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
@@ -822,7 +822,7 @@ void DOPAIR_SUBSET(struct runner *r, struct cell *restrict ci,
                        (float)(piz - pjz)};
         const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
@@ -988,7 +988,7 @@ void DOSELF_SUBSET(struct runner *r, struct cell *restrict ci,
       float dx[3] = {pix[0] - pjx[0], pix[1] - pjx[1], pix[2] - pjx[2]};
       const float r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
       /* Check that particles have been drifted to the current time */
       if (pi->ti_drift != e->ti_current)
         error("Particle pi not drifted to current time");
@@ -1073,15 +1073,16 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
 #ifdef SWIFT_DEBUG_CHECKS
   /* Some constants used to checks that the parts are in the right frame */
+  /* TODO MLADEN: coordinate 2. -> 2.02 with Matthieu */
   const float shift_threshold_x =
-      2. * ci->width[0] +
-      2. * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
+      2.02 * ci->width[0] +
+      2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
   const float shift_threshold_y =
-      2. * ci->width[1] +
-      2. * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
+      2.02 * ci->width[1] +
+      2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
   const float shift_threshold_z =
-      2. * ci->width[2] +
-      2. * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
+      2.02 * ci->width[2] +
+      2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
 #endif /* SWIFT_DEBUG_CHECKS */
 
   /* Get some other useful values. */
@@ -1100,7 +1101,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
   const float H = cosmo->H;
   GET_MU0();
 
-  if (cell_is_active_hydro(ci, e)) {
+  if (CELL_IS_ACTIVE(ci, e)) {
 
     /* Loop over the parts in ci. */
     for (int pid = count_i - 1;
@@ -1111,7 +1112,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
       const float hi = pi->h;
 
       /* Skip inactive particles */
-      if (!part_is_active(pi, e)) continue;
+      if (!PART_IS_ACTIVE(pi, e)) continue;
 
       /* Is there anything we need to interact with ? */
       const double di = sort_i[pid].d + hi * kernel_gamma + dx_max - rshift;
@@ -1168,11 +1169,13 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
               "Invalid particle position in Z for pj (pjz=%e ci->width[2]=%e)",
               pjz, ci->width[2]);
 
+#if defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
         if (pj->ti_drift != e->ti_current)
           error("Particle pj not drifted to current time");
+#endif
 #endif
 
         /* Hit or miss? */
@@ -1197,7 +1200,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
     }   /* loop over the parts in ci. */
   }     /* Cell ci is active */
 
-  if (cell_is_active_hydro(cj, e)) {
+  if (CELL_IS_ACTIVE(cj, e)) {
 
     /* Loop over the parts in cj. */
     for (int pjd = 0; pjd < count_j && sort_j[pjd].d - hj_max - dx_max < di_max;
@@ -1208,7 +1211,7 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
       const float hj = pj->h;
 
       /* Skip inactive particles */
-      if (!part_is_active(pj, e)) continue;
+      if (!PART_IS_ACTIVE(pj, e)) continue;
 
       /* Is there anything we need to interact with ? */
       const double dj = sort_j[pjd].d - hj * kernel_gamma - dx_max + rshift;
@@ -1265,11 +1268,13 @@ void DOPAIR1(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
               "Invalid particle position in Z for pj (pjz=%e ci->width[2]=%e)",
               pjz, ci->width[2]);
 
+#if defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
         if (pj->ti_drift != e->ti_current)
           error("Particle pj not drifted to current time");
+#endif
 #endif
 
         /* Hit or miss? */
@@ -1314,10 +1319,10 @@ void DOPAIR1_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(ci, e) && !cell_is_active_hydro(cj, e)) return;
+  if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
 
   /* Check that cells are drifted. */
-  if (!cell_are_part_drifted(ci, e) || !cell_are_part_drifted(cj, e))
+  if (!CELL_ARE_PART_DRIFTED(ci, e) || !CELL_ARE_PART_DRIFTED(cj, e))
     error("Interacting undrifted cells.");
 
   /* Get the sort ID. */
@@ -1422,15 +1427,16 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
 #ifdef SWIFT_DEBUG_CHECKS
   /* Some constants used to checks that the parts are in the right frame */
+  /* TODO MLADEN: coordinate 2. -> 2.02 with Matthieu */
   const float shift_threshold_x =
-      2. * ci->width[0] +
-      2. * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
+      2.02 * ci->width[0] +
+      2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
   const float shift_threshold_y =
-      2. * ci->width[1] +
-      2. * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
+      2.02 * ci->width[1] +
+      2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
   const float shift_threshold_z =
-      2. * ci->width[2] +
-      2. * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
+      2.02 * ci->width[2] +
+      2.02 * max(ci->hydro.dx_max_part, cj->hydro.dx_max_part);
 #endif /* SWIFT_DEBUG_CHECKS */
 
   /* Get some other useful values. */
@@ -1467,14 +1473,14 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
     /* If everybody is active don't bother copying */
     sort_active_i = sort_i;
     count_active_i = count_i;
-  } else if (cell_is_active_hydro(ci, e)) {
+  } else if (CELL_IS_ACTIVE(ci, e)) {
     if (posix_memalign((void **)&sort_active_i, SWIFT_CACHE_ALIGNMENT,
                        sizeof(struct sort_entry) * count_i) != 0)
       error("Failed to allocate active sortlists.");
 
     /* Collect the active particles in ci */
     for (int k = 0; k < count_i; k++) {
-      if (part_is_active(&parts_i[sort_i[k].i], e)) {
+      if (PART_IS_ACTIVE(&parts_i[sort_i[k].i], e)) {
         sort_active_i[count_active_i] = sort_i[k];
         count_active_i++;
       }
@@ -1486,14 +1492,14 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
     /* If everybody is active don't bother copying */
     sort_active_j = sort_j;
     count_active_j = count_j;
-  } else if (cell_is_active_hydro(cj, e)) {
+  } else if (CELL_IS_ACTIVE(cj, e)) {
     if (posix_memalign((void **)&sort_active_j, SWIFT_CACHE_ALIGNMENT,
                        sizeof(struct sort_entry) * count_j) != 0)
       error("Failed to allocate active sortlists.");
 
     /* Collect the active particles in cj */
     for (int k = 0; k < count_j; k++) {
-      if (part_is_active(&parts_j[sort_j[k].i], e)) {
+      if (PART_IS_ACTIVE(&parts_j[sort_j[k].i], e)) {
         sort_active_j[count_active_j] = sort_j[k];
         count_active_j++;
       }
@@ -1527,7 +1533,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
     /* Do we need to only check active parts in cj
        (i.e. pi does not need updating) ? */
-    if (!part_is_active(pi, e)) {
+    if (!PART_IS_ACTIVE(pi, e)) {
 
       /* Loop over the *active* parts in cj within range of pi */
       for (int pjd = 0; pjd < count_active_j && sort_active_j[pjd].d < di;
@@ -1581,12 +1587,14 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
               "Invalid particle position in Z for pj (pjz=%e ci->width[2]=%e)",
               pjz, ci->width[2]);
 
+#if defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
 
         if (pj->ti_drift != e->ti_current)
           error("Particle pj not drifted to current time");
+#endif
 #endif
 
         /* Hit or miss?
@@ -1659,19 +1667,21 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
               "Invalid particle position in Z for pj (pjz=%e ci->width[2]=%e)",
               pjz, ci->width[2]);
 
+#if defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
 
         if (pj->ti_drift != e->ti_current)
           error("Particle pj not drifted to current time");
+#endif
 #endif
         /* Hit or miss?
            (note that we will do the other condition in the reverse loop) */
         if (r2 < hig2) {
 
           /* Does pj need to be updated too? */
-          if (part_is_active(pj, e)) {
+          if (PART_IS_ACTIVE(pj, e)) {
             IACT(r2, dx, hi, hj, pi, pj, a, H);
             IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
@@ -1733,7 +1743,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
 
     /* Do we need to only check active parts in ci
        (i.e. pj does not need updating) ? */
-    if (!part_is_active(pj, e)) {
+    if (!PART_IS_ACTIVE(pj, e)) {
 
       /* Loop over the *active* parts in ci. */
       for (int pid = count_active_i - 1;
@@ -1788,11 +1798,13 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
               "Invalid particle position in Z for pj (pjz=%e ci->width[2]=%e)",
               pjz, ci->width[2]);
 
+#if defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
         if (pj->ti_drift != e->ti_current)
           error("Particle pj not drifted to current time");
+#endif
 #endif
 
         /* Hit or miss?
@@ -1867,11 +1879,13 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
               "Invalid particle position in Z for pj (pjz=%e ci->width[2]=%e)",
               pjz, ci->width[2]);
 
+#if defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
         if (pj->ti_drift != e->ti_current)
           error("Particle pj not drifted to current time");
+#endif
 #endif
 
         /* Hit or miss?
@@ -1879,7 +1893,7 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
         if (r2 < hjg2 && r2 >= hig2) {
 
           /* Does pi need to be updated too? */
-          if (part_is_active(pi, e)) {
+          if (PART_IS_ACTIVE(pi, e)) {
             IACT(r2, dx, hj, hi, pj, pi, a, H);
             IACT_MHD(r2, dx, hj, hi, pj, pi, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
@@ -1915,9 +1929,9 @@ void DOPAIR2(struct runner *r, struct cell *ci, struct cell *cj, const int sid,
   }     /* Loop over all cj */
 
   /* Clean-up if necessary */  // MATTHIEU: temporary disable this optimization
-  if (cell_is_active_hydro(ci, e))  // && !cell_is_all_active_hydro(ci, e))
+  if (CELL_IS_ACTIVE(ci, e))   // && !cell_is_all_active_hydro(ci, e))
     free(sort_active_i);
-  if (cell_is_active_hydro(cj, e))  // && !cell_is_all_active_hydro(cj, e))
+  if (CELL_IS_ACTIVE(cj, e))  // && !cell_is_all_active_hydro(cj, e))
     free(sort_active_j);
 
   TIMER_TOC(TIMER_DOPAIR);
@@ -1940,10 +1954,10 @@ void DOPAIR2_BRANCH(struct runner *r, struct cell *ci, struct cell *cj) {
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(ci, e) && !cell_is_active_hydro(cj, e)) return;
+  if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
 
   /* Check that cells are drifted. */
-  if (!cell_are_part_drifted(ci, e) || !cell_are_part_drifted(cj, e))
+  if (!CELL_ARE_PART_DRIFTED(ci, e) || !CELL_ARE_PART_DRIFTED(cj, e))
     error("Interacting undrifted cells.");
 
   /* Get the sort ID. */
@@ -2044,7 +2058,7 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
                      count * sizeof(int)) != 0)
     error("Failed to allocate indt.");
   for (int k = 0; k < count; k++)
-    if (part_is_active(&parts[k], e)) {
+    if (PART_IS_ACTIVE(&parts[k], e)) {
       indt[countdt] = k;
       countdt += 1;
     }
@@ -2070,7 +2084,7 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
     const float hig2 = hi * hi * kernel_gamma2;
 
     /* Is the ith particle inactive? */
-    if (!part_is_active(pi, e)) {
+    if (!PART_IS_ACTIVE(pi, e)) {
 
       /* Loop over the other particles .*/
       for (int pjd = firstdt; pjd < countdt; pjd++) {
@@ -2079,7 +2093,7 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
         struct part *restrict pj = &parts[indt[pjd]];
         const float hj = pj->h;
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
@@ -2141,11 +2155,11 @@ void DOSELF1(struct runner *r, struct cell *restrict c) {
           r2 += dx[k] * dx[k];
         }
         const int doj =
-            (part_is_active(pj, e)) && (r2 < hj * hj * kernel_gamma2);
+            (PART_IS_ACTIVE(pj, e)) && (r2 < hj * hj * kernel_gamma2);
 
         const int doi = (r2 < hig2);
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
@@ -2235,14 +2249,14 @@ void DOSELF1_BRANCH(struct runner *r, struct cell *c) {
   if (c->hydro.count == 0) return;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(c, e)) return;
+  if (!CELL_IS_ACTIVE(c, e)) return;
 
   /* Did we mess up the recursion? */
   if (c->hydro.h_max_old * kernel_gamma > c->dmin)
     error("Cell smaller than smoothing length");
 
   /* Check that cells are drifted. */
-  if (!cell_are_part_drifted(c, e)) error("Interacting undrifted cell.");
+  if (!CELL_ARE_PART_DRIFTED(c, e)) error("Interacting undrifted cell.");
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
   DOSELF1_NAIVE(r, c);
@@ -2282,7 +2296,7 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
                      count * sizeof(int)) != 0)
     error("Failed to allocate indt.");
   for (int k = 0; k < count; k++)
-    if (part_is_active(&parts[k], e)) {
+    if (PART_IS_ACTIVE(&parts[k], e)) {
       indt[countdt] = k;
       countdt += 1;
     }
@@ -2308,7 +2322,7 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
     const float hig2 = hi * hi * kernel_gamma2;
 
     /* Is the ith particle not active? */
-    if (!part_is_active(pi, e)) {
+    if (!PART_IS_ACTIVE(pi, e)) {
 
       /* Loop over the other particles .*/
       for (int pjd = firstdt; pjd < countdt; pjd++) {
@@ -2325,7 +2339,7 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
           r2 += dx[k] * dx[k];
         }
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
@@ -2379,7 +2393,7 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
           r2 += dx[k] * dx[k];
         }
 
-#ifdef SWIFT_DEBUG_CHECKS
+#if defined(SWIFT_DEBUG_CHECKS) && defined(DO_DRIFT_DEBUG_CHECKS)
         /* Check that particles have been drifted to the current time */
         if (pi->ti_drift != e->ti_current)
           error("Particle pi not drifted to current time");
@@ -2391,7 +2405,7 @@ void DOSELF2(struct runner *r, struct cell *restrict c) {
         if (r2 < hig2 || r2 < hj * hj * kernel_gamma2) {
 
           /* Does pj need to be updated too? */
-          if (part_is_active(pj, e)) {
+          if (PART_IS_ACTIVE(pj, e)) {
             IACT(r2, dx, hi, hj, pi, pj, a, H);
             IACT_MHD(r2, dx, hi, hj, pi, pj, mu_0, a, H);
 #if (FUNCTION_TASK_LOOP == TASK_LOOP_DENSITY)
@@ -2447,14 +2461,14 @@ void DOSELF2_BRANCH(struct runner *r, struct cell *c) {
   if (c->hydro.count == 0) return;
 
   /* Anything to do here? */
-  if (!cell_is_active_hydro(c, e)) return;
+  if (!CELL_IS_ACTIVE(c, e)) return;
 
   /* Did we mess up the recursion? */
   if (c->hydro.h_max_old * kernel_gamma > c->dmin)
     error("Cell smaller than smoothing length");
 
   /* Check that cells are drifted. */
-  if (!cell_are_part_drifted(c, e)) error("Interacting undrifted cell.");
+  if (!CELL_ARE_PART_DRIFTED(c, e)) error("Interacting undrifted cell.");
 
 #if defined(SWIFT_USE_NAIVE_INTERACTIONS)
   DOSELF2_NAIVE(r, c);
@@ -2486,7 +2500,7 @@ void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
   TIMER_TIC;
 
   /* Should we even bother? */
-  if (!cell_is_active_hydro(ci, e) && !cell_is_active_hydro(cj, e)) return;
+  if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
 
   /* Get the type of pair and flip ci/cj if needed. */
@@ -2506,10 +2520,10 @@ void DOSUB_PAIR1(struct runner *r, struct cell *ci, struct cell *cj,
   }
 
   /* Otherwise, compute the pair directly. */
-  else if (cell_is_active_hydro(ci, e) || cell_is_active_hydro(cj, e)) {
+  else if (CELL_IS_ACTIVE(ci, e) || CELL_IS_ACTIVE(cj, e)) {
 
     /* Make sure both cells are drifted to the current timestep. */
-    if (!cell_are_part_drifted(ci, e) || !cell_are_part_drifted(cj, e))
+    if (!CELL_ARE_PART_DRIFTED(ci, e) || !CELL_ARE_PART_DRIFTED(cj, e))
       error("Interacting undrifted cells.");
 
     /* Do any of the cells need to be sorted first? */
@@ -2545,7 +2559,7 @@ void DOSUB_SELF1(struct runner *r, struct cell *ci, int gettimer) {
   TIMER_TIC;
 
   /* Should we even bother? */
-  if (ci->hydro.count == 0 || !cell_is_active_hydro(ci, r->e)) return;
+  if (ci->hydro.count == 0 || !CELL_IS_ACTIVE(ci, r->e)) return;
 
   /* Recurse? */
   if (cell_can_recurse_in_self_hydro_task(ci)) {
@@ -2564,7 +2578,7 @@ void DOSUB_SELF1(struct runner *r, struct cell *ci, int gettimer) {
   else {
 
     /* Drift the cell to the current timestep if needed. */
-    if (!cell_are_part_drifted(ci, r->e)) error("Interacting undrifted cell.");
+    if (!CELL_ARE_PART_DRIFTED(ci, r->e)) error("Interacting undrifted cell.");
 
     DOSELF1_BRANCH(r, ci);
   }
@@ -2592,7 +2606,7 @@ void DOSUB_PAIR2(struct runner *r, struct cell *ci, struct cell *cj,
   TIMER_TIC;
 
   /* Should we even bother? */
-  if (!cell_is_active_hydro(ci, e) && !cell_is_active_hydro(cj, e)) return;
+  if (!CELL_IS_ACTIVE(ci, e) && !CELL_IS_ACTIVE(cj, e)) return;
   if (ci->hydro.count == 0 || cj->hydro.count == 0) return;
 
   /* Get the type of pair and flip ci/cj if needed. */
@@ -2612,10 +2626,10 @@ void DOSUB_PAIR2(struct runner *r, struct cell *ci, struct cell *cj,
   }
 
   /* Otherwise, compute the pair directly. */
-  else if (cell_is_active_hydro(ci, e) || cell_is_active_hydro(cj, e)) {
+  else if (CELL_IS_ACTIVE(ci, e) || CELL_IS_ACTIVE(cj, e)) {
 
     /* Make sure both cells are drifted to the current timestep. */
-    if (!cell_are_part_drifted(ci, e) || !cell_are_part_drifted(cj, e))
+    if (!CELL_ARE_PART_DRIFTED(ci, e) || !CELL_ARE_PART_DRIFTED(cj, e))
       error("Interacting undrifted cells.");
 
     /* Do any of the cells need to be sorted first? */
@@ -2651,7 +2665,7 @@ void DOSUB_SELF2(struct runner *r, struct cell *ci, int gettimer) {
   TIMER_TIC;
 
   /* Should we even bother? */
-  if (ci->hydro.count == 0 || !cell_is_active_hydro(ci, r->e)) return;
+  if (ci->hydro.count == 0 || !CELL_IS_ACTIVE(ci, r->e)) return;
 
   /* Recurse? */
   if (cell_can_recurse_in_self_hydro_task(ci)) {
diff --git a/src/runner_doiact_hydro.h b/src/runner_doiact_hydro.h
index 04ca44bfac3dd8c3122ba99cbbf266b7483f5b32..04824e87eecc32f91aa4352176212c853ceaf56b 100644
--- a/src/runner_doiact_hydro.h
+++ b/src/runner_doiact_hydro.h
@@ -124,6 +124,23 @@
   {}
 #endif
 
+#if (FUNCTION_TASK_LOOP == TASK_LOOP_RT_GRADIENT) || \
+    (FUNCTION_TASK_LOOP == TASK_LOOP_RT_TRANSPORT)
+/* RT specific function calls */
+#define PART_IS_ACTIVE part_is_rt_active
+#define CELL_IS_ACTIVE cell_is_rt_active
+#define CELL_ARE_PART_DRIFTED cell_are_part_drifted_rt_sub_cycle
+#else
+/* default hydro behaviour. */
+#define PART_IS_ACTIVE part_is_active
+#define CELL_IS_ACTIVE cell_is_active_hydro
+#define CELL_ARE_PART_DRIFTED cell_are_part_drifted
+/* when running with RT subcycling, we can have RT active
+ * particles in a normal swift step that aren't drifted to
+ * the current time, so we don't do those checks there. */
+#define DO_DRIFT_DEBUG_CHECKS 1
+#endif
+
 #define _IACT_NONSYM_VEC(f) PASTE(runner_iact_nonsym_vec, f)
 #define IACT_NONSYM_VEC _IACT_NONSYM_VEC(FUNCTION)
 
diff --git a/src/runner_doiact_undef.h b/src/runner_doiact_undef.h
index 7ab48277d31fd67086fd8f11b67657fe986308a5..0a76c6d15df5a535a6cf7e93f6b54a9972b60bfa 100644
--- a/src/runner_doiact_undef.h
+++ b/src/runner_doiact_undef.h
@@ -33,3 +33,9 @@
 #undef GET_MU0
 #undef FUNCTION
 #undef FUNCTION_TASK_LOOP
+
+/* these are defined in runner_doiact_functions_hydro.h at every #include */
+#undef PART_IS_ACTIVE
+#undef CELL_IS_ACTIVE
+#undef CELL_ARE_PART_DRIFTED
+#undef DO_DRIFT_DEBUG_CHECKS
diff --git a/src/runner_ghost.c b/src/runner_ghost.c
index 8750bd5089c65e82eefa02ad7ab2596e3b7c56b0..2c860e417ba9dc10407df3d986b94c64dbc0172e 100644
--- a/src/runner_ghost.c
+++ b/src/runner_ghost.c
@@ -1091,6 +1091,7 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
   const struct hydro_props *hydro_props = e->hydro_properties;
 
   const int with_cosmology = (e->policy & engine_policy_cosmology);
+  const int with_rt = (e->policy & engine_policy_rt);
 
   const float hydro_h_max = e->hydro_properties->h_max;
   const float hydro_h_min = e->hydro_properties->h_min;
@@ -1294,7 +1295,12 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
 
 #endif /* EXTRA_HYDRO_LOOP */
 
-            rt_reset_part(p, cosmo);
+            if (with_rt) {
+#ifdef SWIFT_RT_DEBUG_CHECKS
+              rt_debugging_check_nr_subcycles(p, e->rt_props);
+#endif
+              rt_reset_part(p, cosmo);
+            }
 
             /* Ok, we are done with this particle */
             continue;
@@ -1466,7 +1472,12 @@ void runner_do_ghost(struct runner *r, struct cell *c, int timer) {
 
 #endif /* EXTRA_HYDRO_LOOP */
 
-        rt_reset_part(p, cosmo);
+        if (with_rt) {
+#ifdef SWIFT_RT_DEBUG_CHECKS
+          rt_debugging_check_nr_subcycles(p, e->rt_props);
+#endif
+          rt_reset_part(p, cosmo);
+        }
       }
 
       /* We now need to treat the particles whose smoothing length had not
@@ -1587,7 +1598,7 @@ void runner_do_rt_ghost1(struct runner *r, struct cell *c, int timer) {
 
   /* Anything to do here? */
   if (count == 0) return;
-  if (!cell_is_active_hydro(c, e)) return;
+  if (!cell_is_rt_active(c, e)) return;
 
   TIMER_TIC;
 
@@ -1607,8 +1618,13 @@ void runner_do_rt_ghost1(struct runner *r, struct cell *c, int timer) {
       if (part_is_inhibited(p, e)) continue;
 
       /* Skip inactive parts */
-      if (!part_is_active(p, e)) continue;
+      if (!part_is_rt_active(p, e)) continue;
 
+      /* First reset everything that needs to be reset for the following
+       * subcycle */
+      rt_reset_part_each_subcycle(p);
+
+      /* Now finish up injection */
       rt_finalise_injection(p, e->rt_props);
     }
   }
@@ -1632,7 +1648,7 @@ void runner_do_rt_ghost2(struct runner *r, struct cell *c, int timer) {
 
   /* Anything to do here? */
   if (count == 0) return;
-  if (!cell_is_active_hydro(c, e)) return;
+  if (!cell_is_rt_active(c, e)) return;
 
   TIMER_TIC;
 
@@ -1652,7 +1668,7 @@ void runner_do_rt_ghost2(struct runner *r, struct cell *c, int timer) {
       if (part_is_inhibited(p, e)) continue;
 
       /* Skip inactive parts */
-      if (!part_is_active(p, e)) continue;
+      if (!part_is_rt_active(p, e)) continue;
 
       rt_end_gradient(p, cosmo);
     }
diff --git a/src/runner_main.c b/src/runner_main.c
index 022ffd764e5d2eba2c5a9c2a286ec048f172046c..f8a33bcbcc4df6f399c8215aaef597ef543fe757 100644
--- a/src/runner_main.c
+++ b/src/runner_main.c
@@ -388,7 +388,19 @@ void *runner_main(void *data) {
           /* Cleanup only if any of the indices went stale. */
           runner_do_hydro_sort(
               r, ci, t->flags,
-              ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin, 1);
+              ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin,
+              cell_get_flag(ci, cell_flag_rt_requests_sort), 1);
+          /* Reset the sort flags as our work here is done. */
+          t->flags = 0;
+          break;
+        case task_type_rt_sort:
+          /* Cleanup only if any of the indices went stale.
+           * NOTE: we check whether we reset the sort flags when the
+           * recv tasks are running. Cells without an RT recv task
+           * don't have rt_sort tasks. */
+          runner_do_hydro_sort(
+              r, ci, t->flags,
+              ci->hydro.dx_max_sort_old > space_maxreldx * ci->dmin, 1, 1);
           /* Reset the sort flags as our work here is done. */
           t->flags = 0;
           break;
@@ -462,6 +474,9 @@ void *runner_main(void *data) {
         case task_type_collect:
           runner_do_timestep_collect(r, ci, 1);
           break;
+        case task_type_rt_collect_times:
+          runner_do_collect_rt_times(r, ci, 1);
+          break;
 #ifdef WITH_MPI
         case task_type_send:
           if (t->subtype == task_subtype_tend) {
@@ -491,7 +506,7 @@ void *runner_main(void *data) {
           } else if (t->subtype == task_subtype_gradient) {
             runner_do_recv_part(r, ci, 0, 1);
           } else if (t->subtype == task_subtype_rt_gradient) {
-            runner_do_recv_part(r, ci, 0, 1);
+            runner_do_recv_part(r, ci, 2, 1);
           } else if (t->subtype == task_subtype_rt_transport) {
             runner_do_recv_part(r, ci, 0, 1);
           } else if (t->subtype == task_subtype_part_swallow) {
@@ -576,6 +591,9 @@ void *runner_main(void *data) {
         case task_type_rt_tchem:
           runner_do_rt_tchem(r, t->ci, 1);
           break;
+        case task_type_rt_advance_cell_time:
+          runner_do_rt_advance_cell_time(r, t->ci, 1);
+          break;
         default:
           error("Unknown/invalid task type (%d).", t->type);
       }
diff --git a/src/runner_others.c b/src/runner_others.c
index d0edab4e88c787b55cdd2b1af9ba5340dccd72b9..efb728fa409a1f4ed17de548ce53fcee6ff7f01e 100644
--- a/src/runner_others.c
+++ b/src/runner_others.c
@@ -1022,7 +1022,7 @@ void runner_do_rt_tchem(struct runner *r, struct cell *c, int timer) {
 
   /* Anything to do here? */
   if (count == 0) return;
-  if (!cell_is_active_hydro(c, e)) return;
+  if (!cell_is_rt_active(c, e)) return;
 
   TIMER_TIC;
 
@@ -1032,7 +1032,6 @@ void runner_do_rt_tchem(struct runner *r, struct cell *c, int timer) {
       if (c->progeny[k] != NULL) runner_do_rt_tchem(r, c->progeny[k], 0);
   } else {
 
-    /* const struct cosmology *cosmo = e->cosmology; */
     struct part *restrict parts = c->hydro.parts;
     struct xpart *restrict xparts = c->hydro.xparts;
 
@@ -1047,26 +1046,29 @@ void runner_do_rt_tchem(struct runner *r, struct cell *c, int timer) {
       if (part_is_inhibited(p, e)) continue;
 
       /* Skip inactive parts */
-      if (!part_is_active(p, e)) continue;
+      if (!part_is_rt_active(p, e)) continue;
 
       /* Finish the force loop */
-      const integertime_t ti_current = e->ti_current;
-      const integertime_t ti_step = get_integer_timestep(p->time_bin);
-      const integertime_t ti_begin =
-          get_integer_time_begin(ti_current + 1, p->time_bin);
+      const integertime_t ti_current_subcycle = e->ti_current_subcycle;
+      const integertime_t ti_step =
+          get_integer_timestep(p->rt_time_data.time_bin);
+      const integertime_t ti_begin = get_integer_time_begin(
+          ti_current_subcycle + 1, p->rt_time_data.time_bin);
       const integertime_t ti_end = ti_begin + ti_step;
 
+      const double dt =
+          rt_part_dt(ti_begin, ti_end, e->time_base, with_cosmology, cosmo);
 #ifdef SWIFT_DEBUG_CHECKS
-      if (ti_begin != ti_current)
+      if (ti_begin != ti_current_subcycle)
         error(
             "Particle in wrong time-bin, ti_end=%lld, ti_begin=%lld, "
             "ti_step=%lld time_bin=%d wakeup=%d ti_current=%lld",
             ti_end, ti_begin, ti_step, p->time_bin, p->limiter_data.wakeup,
-            ti_current);
+            ti_current_subcycle);
+      if (dt < 0.)
+        error("Got part with negative time-step: %lld, %.6g", p->id, dt);
 #endif
 
-      const double dt = rt_part_dt(ti_begin, ti_end, e->time_base,
-                                   with_cosmology, e->cosmology);
       rt_finalise_transport(p, dt, cosmo);
 
       /* And finally do thermochemistry */
@@ -1074,5 +1076,5 @@ void runner_do_rt_tchem(struct runner *r, struct cell *c, int timer) {
     }
   }
 
-  if (timer) TIMER_TOC(timer_end_rt_tchem);
+  if (timer) TIMER_TOC(timer_do_rt_tchem);
 }
diff --git a/src/runner_recv.c b/src/runner_recv.c
index 17b719a182cce027d3e234bf7bc8c7af5a4be67d..f225e1e6d6c7567b9cc06fbfccdae70e6b4e0017 100644
--- a/src/runner_recv.c
+++ b/src/runner_recv.c
@@ -63,6 +63,15 @@ void runner_do_recv_part(struct runner *r, struct cell *c, int clear_sorts,
   if (c->nodeID == engine_rank) error("Updating a local cell!");
 #endif
 
+  if (clear_sorts == 2) {
+    /* This is a call for the first RT recv task. Check whether
+     * we need to clear the sorts now. In the case of a foreign
+     * cell where no xv comms are done, but RT is active, we
+     * need to force a sort after the gradient recv. */
+    clear_sorts = !cell_get_flag(c, cell_flag_skip_rt_sort);
+    cell_clear_flag(c, cell_flag_skip_rt_sort);
+  }
+
   /* Clear this cell's sorted mask. */
   if (clear_sorts) c->hydro.sorted = 0;
 
diff --git a/src/runner_sort.c b/src/runner_sort.c
index 3e965fab8609d632799b92624c1f185facc4b977..550626517f861769bd38fa64db04a2b0270b81fe 100644
--- a/src/runner_sort.c
+++ b/src/runner_sort.c
@@ -193,11 +193,13 @@ RUNNER_CHECK_SORTS(stars)
  * @param flags Cell flag.
  * @param cleanup If true, re-build the sorts for the selected flags instead
  *        of just adding them.
+ * @param rt_requests_sort whether this sort was requested for RT. If true,
+ *        this cell is allowed to be undrifted.
  * @param clock Flag indicating whether to record the timing or not, needed
  *      for recursive calls.
  */
 void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
-                          int cleanup, int clock) {
+                          int cleanup, int rt_requests_sort, int clock) {
 
   struct sort_entry *fingers[8];
   const int count = c->hydro.count;
@@ -218,17 +220,22 @@ void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
   } else {
     flags &= ~c->hydro.sorted;
   }
-  if (flags == 0 && !cell_get_flag(c, cell_flag_do_hydro_sub_sort)) return;
+  if (flags == 0 && !cell_get_flag(c, cell_flag_do_hydro_sub_sort) &&
+      !cell_get_flag(c, cell_flag_do_rt_sub_sort))
+    return;
 
   /* Check that the particles have been moved to the current time */
-  if (flags && !cell_are_part_drifted(c, r->e))
-    error("Sorting un-drifted cell c->nodeID=%d", c->nodeID);
+  if (flags && !cell_are_part_drifted(c, r->e)) {
+    /* If the sort was requested by RT, cell may be intentionally
+     * undrifted. */
+    if (!rt_requests_sort) error("Sorting un-drifted cell");
+  }
 
 #ifdef SWIFT_DEBUG_CHECKS
   /* Make sure the sort flags are consistent (downward). */
   runner_check_sorts_hydro(c, c->hydro.sorted);
 
-  /* Make sure the sort flags are consistent (upard). */
+  /* Make sure the sort flags are consistent (upward). */
   for (struct cell *finger = c->parent; finger != NULL;
        finger = finger->parent) {
     if (finger->hydro.sorted & ~c->hydro.sorted)
@@ -259,7 +266,7 @@ void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
               r, c->progeny[k], flags,
               cleanup && (c->progeny[k]->hydro.dx_max_sort_old >
                           space_maxreldx * c->progeny[k]->dmin),
-              0);
+              rt_requests_sort, 0);
           dx_max_sort = max(dx_max_sort, c->progeny[k]->hydro.dx_max_sort);
           dx_max_sort_old =
               max(dx_max_sort_old, c->progeny[k]->hydro.dx_max_sort_old);
@@ -416,6 +423,8 @@ void runner_do_hydro_sort(struct runner *r, struct cell *c, int flags,
   /* Clear the cell's sort flags. */
   c->hydro.do_sort = 0;
   cell_clear_flag(c, cell_flag_do_hydro_sub_sort);
+  cell_clear_flag(c, cell_flag_do_rt_sub_sort);
+  cell_clear_flag(c, cell_flag_rt_requests_sort);
   c->hydro.requires_sorts = 0;
 
   if (clock) TIMER_TOC(timer_dosort);
@@ -673,7 +682,8 @@ void runner_do_all_hydro_sort(struct runner *r, struct cell *c) {
   if (c->hydro.super == c) {
 
     /* Sort everything */
-    runner_do_hydro_sort(r, c, 0x1FFF, /*cleanup=*/0, /*timer=*/0);
+    runner_do_hydro_sort(r, c, 0x1FFF, /*cleanup=*/0, /*rt_requests_sort=*/0,
+                         /*timer=*/0);
 
   } else {
 
diff --git a/src/runner_time_integration.c b/src/runner_time_integration.c
index a72b2385a81b39545c0319bb33c92412af8dd906..34c0eccb639e294df1ec9e37792434d32138f28b 100644
--- a/src/runner_time_integration.c
+++ b/src/runner_time_integration.c
@@ -636,12 +636,14 @@ void runner_do_kick2(struct runner *r, struct cell *c, const int timer) {
 void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
   const struct engine *e = r->e;
   const integertime_t ti_current = e->ti_current;
+  const integertime_t ti_current_subcycle = e->ti_current_subcycle;
   const struct cosmology *cosmo = e->cosmology;
   const struct feedback_props *feedback_props = e->feedback_props;
   const struct unit_system *us = e->internal_units;
   const struct phys_const *phys_const = e->physical_constants;
   const int with_cosmology = (e->policy & engine_policy_cosmology);
   const int with_feedback = (e->policy & engine_policy_feedback);
+  const int with_rt = (e->policy & engine_policy_rt);
   const int count = c->hydro.count;
   const int gcount = c->grav.count;
   const int scount = c->stars.count;
@@ -660,11 +662,14 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
   if (!cell_is_active_hydro(c, e) && !cell_is_active_gravity(c, e) &&
       !cell_is_active_stars(c, e) && !cell_is_active_sinks(c, e) &&
       !cell_is_active_black_holes(c, e)) {
+    /* Note: cell_is_rt_active is deliberately skipped. We only change
+     * the RT subcycling time steps when particles are hydro active. */
     c->hydro.updated = 0;
     c->grav.updated = 0;
     c->stars.updated = 0;
     c->sinks.updated = 0;
     c->black_holes.updated = 0;
+    c->rt.updated = 0;
     return;
   }
 
@@ -672,6 +677,8 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
       b_updated = 0;
   integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_end_max = 0,
                 ti_hydro_beg_max = 0;
+  integertime_t ti_rt_end_min = max_nr_timesteps, ti_rt_beg_max = 0;
+  integertime_t ti_rt_min_step_size = max_nr_timesteps;
   integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_end_max = 0,
                 ti_gravity_beg_max = 0;
   integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_end_max = 0,
@@ -701,13 +708,25 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
 
         if (ti_end != ti_current)
           error("Computing time-step of rogue particle.");
+
+        if (with_rt) {
+          const integertime_t ti_rt_end = get_integer_time_end(
+              ti_current_subcycle, p->rt_time_data.time_bin);
+          if (ti_rt_end != ti_current_subcycle)
+            error("Computing RT time-step of rogue particle");
+        }
 #endif
 
         /* Get new time-step */
         const integertime_t ti_new_step = get_part_timestep(p, xp, e);
+        /* ToDo: For now, this needs to be done before we update the particle's
+         * time bins. When we aren't using a fixed number of sub-cycles, we
+         * can move this down with the rest of the RT block. */
+        integertime_t ti_rt_new_step = get_part_rt_timestep(p, xp, e);
 
         /* Update particle */
         p->time_bin = get_time_bin(ti_new_step);
+
         if (p->gpart != NULL) p->gpart->time_bin = p->time_bin;
 
         /* Update the tracers properties */
@@ -737,6 +756,20 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
           /* What is the next starting point for this cell ? */
           ti_gravity_beg_max = max(ti_current, ti_gravity_beg_max);
         }
+
+        /* Same for RT */
+        if (with_rt) {
+          /* Enforce RT time-step size <= hydro step size */
+          ti_rt_new_step = min(ti_new_step, ti_rt_new_step);
+
+          p->rt_time_data.time_bin = get_time_bin(ti_rt_new_step);
+
+          ti_rt_end_min =
+              min(ti_current_subcycle + ti_rt_new_step, ti_rt_end_min);
+          ti_rt_beg_max =
+              max(ti_current_subcycle + ti_rt_new_step, ti_rt_beg_max);
+          ti_rt_min_step_size = min(ti_rt_min_step_size, ti_rt_new_step);
+        }
       }
 
       else { /* part is inactive */
@@ -756,6 +789,28 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
           /* What is the next starting point for this cell ? */
           ti_hydro_beg_max = max(ti_beg, ti_hydro_beg_max);
 
+          /* Same for RT. */
+          if (with_rt) {
+            /* Here we assume that the particle is inactive, which is true for
+             * hydro, but not necessarily for a RT subcycle. RT time steps are
+             * only changed while the particle is hydro active. This allows to
+             * end up with results ti_rt_end == ti_current_subcyle, so we need
+             * to pretend we're past ti_current_subcycle already. */
+            integertime_t ti_rt_end = get_integer_time_end(
+                ti_current_subcycle + 1, p->rt_time_data.time_bin);
+
+            const integertime_t ti_rt_beg = get_integer_time_begin(
+                ti_current_subcycle + 1, p->rt_time_data.time_bin);
+
+            ti_rt_end_min = min(ti_rt_end, ti_rt_end_min);
+            ti_rt_beg_max = max(ti_rt_beg, ti_rt_beg_max);
+            /* We mustn't update ti_rt_min_step_size here, since the RT time
+             * step sizes don't change for particles when they are inactive.
+             * Leaving them here effectively prohibits them from ever increasing
+             * again. Instead, if we're working on a cell where each particle
+             * is inactive, do an appropriate check at the end. */
+          }
+
           if (p->gpart != NULL) {
 
             /* What is the next sync-point ? */
@@ -1041,6 +1096,11 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
         ti_hydro_end_min = min(cp->hydro.ti_end_min, ti_hydro_end_min);
         ti_hydro_beg_max = max(cp->hydro.ti_beg_max, ti_hydro_beg_max);
 
+        ti_rt_end_min = min(cp->rt.ti_rt_end_min, ti_rt_end_min);
+        ti_rt_beg_max = max(cp->rt.ti_rt_beg_max, ti_rt_beg_max);
+        ti_rt_min_step_size =
+            min(cp->rt.ti_rt_min_step_size, ti_rt_min_step_size);
+
         ti_gravity_end_min = min(cp->grav.ti_end_min, ti_gravity_end_min);
         ti_gravity_beg_max = max(cp->grav.ti_beg_max, ti_gravity_beg_max);
 
@@ -1064,9 +1124,19 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
   c->stars.updated = s_updated;
   c->sinks.updated = sink_updated;
   c->black_holes.updated = b_updated;
+  /* We don't count the RT updates here because the
+   * timestep tasks aren't active during sub-cycles.
+   * We do that in rt_advanced_cell_time instead. */
 
   c->hydro.ti_end_min = ti_hydro_end_min;
   c->hydro.ti_beg_max = ti_hydro_beg_max;
+  c->rt.ti_rt_end_min = ti_rt_end_min;
+  c->rt.ti_rt_beg_max = ti_rt_beg_max;
+  if (cell_is_starting_hydro(c, e)) {
+    /* We only change the RT time steps when the cell is also hydro active.
+     * Without this check here, ti_rt_min_step_size = max_nr_steps... */
+    c->rt.ti_rt_min_step_size = ti_rt_min_step_size;
+  }
   c->grav.ti_end_min = ti_gravity_end_min;
   c->grav.ti_beg_max = ti_gravity_beg_max;
   c->stars.ti_end_min = ti_stars_end_min;
@@ -1092,6 +1162,12 @@ void runner_do_timestep(struct runner *r, struct cell *c, const int timer) {
   if (c->black_holes.ti_end_min == e->ti_current &&
       c->black_holes.ti_end_min < max_nr_timesteps)
     error("End of next black holes step is current time!");
+  /* Contrary to sinks, stars, bhs etc, we may have "rt particles"
+   * without running with RT. So additional if (with_rt) check is
+   * needed here. */
+  if (with_rt && (c->rt.ti_rt_end_min == e->ti_current &&
+                  c->rt.ti_rt_end_min < max_nr_timesteps))
+    error("Cell %lld End of next RT step is current time!", c->cellID);
 #endif
 
   if (timer) TIMER_TOC(timer_timestep);
@@ -1118,7 +1194,10 @@ void runner_do_timestep_collect(struct runner *r, struct cell *c,
   size_t s_updated = 0;
   size_t b_updated = 0;
   size_t si_updated = 0;
+  size_t rt_updated = 0;
+
   integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_beg_max = 0;
+  integertime_t ti_rt_end_min = max_nr_timesteps, ti_rt_beg_max = 0;
   integertime_t ti_grav_end_min = max_nr_timesteps, ti_grav_beg_max = 0;
   integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_beg_max = 0;
   integertime_t ti_black_holes_end_min = max_nr_timesteps,
@@ -1136,6 +1215,8 @@ void runner_do_timestep_collect(struct runner *r, struct cell *c,
       /* And update */
       ti_hydro_end_min = min(ti_hydro_end_min, cp->hydro.ti_end_min);
       ti_hydro_beg_max = max(ti_hydro_beg_max, cp->hydro.ti_beg_max);
+      ti_rt_end_min = min(cp->rt.ti_rt_end_min, ti_rt_end_min);
+      ti_rt_beg_max = max(cp->rt.ti_rt_beg_max, ti_rt_beg_max);
       ti_grav_end_min = min(ti_grav_end_min, cp->grav.ti_end_min);
       ti_grav_beg_max = max(ti_grav_beg_max, cp->grav.ti_beg_max);
       ti_stars_end_min = min(ti_stars_end_min, cp->stars.ti_end_min);
@@ -1152,6 +1233,7 @@ void runner_do_timestep_collect(struct runner *r, struct cell *c,
       s_updated += cp->stars.updated;
       b_updated += cp->black_holes.updated;
       si_updated += cp->sinks.updated;
+      rt_updated += cp->rt.updated;
 
       /* Collected, so clear for next time. */
       cp->hydro.updated = 0;
@@ -1159,12 +1241,15 @@ void runner_do_timestep_collect(struct runner *r, struct cell *c,
       cp->stars.updated = 0;
       cp->black_holes.updated = 0;
       cp->sinks.updated = 0;
+      cp->rt.updated = 0;
     }
   }
 
   /* Store the collected values in the cell. */
   c->hydro.ti_end_min = ti_hydro_end_min;
   c->hydro.ti_beg_max = ti_hydro_beg_max;
+  c->rt.ti_rt_end_min = ti_rt_end_min;
+  c->rt.ti_rt_beg_max = ti_rt_beg_max;
   c->grav.ti_end_min = ti_grav_end_min;
   c->grav.ti_beg_max = ti_grav_beg_max;
   c->stars.ti_end_min = ti_stars_end_min;
@@ -1179,6 +1264,7 @@ void runner_do_timestep_collect(struct runner *r, struct cell *c,
   c->stars.updated = s_updated;
   c->black_holes.updated = b_updated;
   c->sinks.updated = si_updated;
+  c->rt.updated = rt_updated;
 }
 
 /**
@@ -1492,3 +1578,157 @@ void runner_do_sync(struct runner *r, struct cell *c, int force,
 
   if (timer) TIMER_TOC(timer_do_sync);
 }
+
+/**
+ * @brief Update the cell's t_rt_end_min so that the sub-cycling can proceed
+ * with correct cell times. (During sub-cycles, the regular timestep and
+ * timestep_collect tasks do not run. This replaces the collection of cell
+ * times of timestep tasks during sub-cycles. )
+ *
+ * @param r The #runner thread.
+ * @param c The #cell.
+ * @param timer Are we timing this ?
+ */
+void runner_do_rt_advance_cell_time(struct runner *r, struct cell *c,
+                                    int timer) {
+
+  struct engine *e = r->e;
+  const int count = c->hydro.count;
+  /* Reset update count regardless whether cell is active or not */
+  c->rt.updated = 0;
+
+#ifdef SWIFT_RT_DEBUG_CHECKS
+  if (c->super == c) c->rt.advanced_time = 1;
+#endif
+
+  /* Anything to do here? */
+  if (count == 0) return;
+  if (!cell_is_rt_active(c, e)) return;
+
+  TIMER_TIC;
+
+  int rt_updated = 0;
+
+  if (c->split) {
+    for (int k = 0; k < 8; k++) {
+      struct cell *cp = c->progeny[k];
+      if (cp != NULL) {
+        runner_do_rt_advance_cell_time(r, cp, 0);
+        rt_updated += cp->rt.updated;
+      }
+    }
+  } else {
+    /* Do some debugging stuff on active particles before setting the cell time.
+     * This test is not reliable on foreign cells. After a rebuild, we may end
+     * up with an active foreign cell which was not updated in this step because
+     * it had no active neighbouring cells, and its particle data may be random
+     * junk. */
+
+    if (c->nodeID == engine_rank) {
+      struct part *restrict parts = c->hydro.parts;
+
+      /* Loop over the gas particles in this cell. */
+      for (int i = 0; i < count; i++) {
+
+        /* Get a handle on the part. */
+        struct part *restrict p = &parts[i];
+
+        /* Skip inhibited parts */
+        if (part_is_inhibited(p, e)) continue;
+
+        /* Skip inactive parts */
+        if (!part_is_rt_active(p, e)) continue;
+
+#ifdef SWIFT_RT_DEBUG_CHECKS
+        /* Run checks. */
+        rt_debug_sequence_check(p, 5, __func__);
+        /* Mark that the subcycling has happened */
+        rt_debugging_count_subcycle(p);
+#endif
+        rt_updated++;
+      }
+    }
+  }
+
+  c->rt.updated = rt_updated;
+
+  /* Note: c->rt.ti_rt_min_step_size may be greater than
+   * c->super->rt.ti_rt_min_step_size. This is expected behaviour.
+   * We only update the cell's own time after it's been active. */
+  c->rt.ti_rt_end_min += c->rt.ti_rt_min_step_size;
+
+  if (timer) TIMER_TOC(timer_do_rt_advance_cell_time);
+}
+
+/**
+ * @brief Recursively collect the end-of-timestep information from the top-level
+ * to the super level for the RT sub-cycling. (During sub-cycles, the regular
+ * timestep and timestep_collect tasks do not run. This replaces the
+ * timestep_collect task.)
+ *
+ * @param r The runner thread.
+ * @param c The cell.
+ * @param timer Are we timing this ?
+ */
+void runner_do_collect_rt_times(struct runner *r, struct cell *c,
+                                const int timer) {
+
+  const struct engine *e = r->e;
+  size_t rt_updated = 0;
+
+  if (e->ti_current == e->ti_current_subcycle)
+    error("called collect_rt_times during a main step");
+
+  /* Early stop if we are at the super level.
+   * The time-step/rt_advance_cell_time tasks would have set things at
+   * this level already. */
+
+  if (c->super == c) {
+#ifdef SWIFT_RT_DEBUG_CHECKS
+    /* Do a check before the early exit.
+     * rt_advanced_cell_time should be called exactly once before
+     * collect times. Except on the first subcycle, because the
+     * collect_rt_times task shouldn't be called in the main steps.
+     * In that case, it should be exactly 2. */
+    if (e->ti_current_subcycle - c->rt.ti_rt_end_min == e->ti_current) {
+      /* This is the first subcycle */
+      if (c->rt.advanced_time != 2)
+        error("Called cell with wrong advanced_time counter. Expect=2, got=%d",
+              c->rt.advanced_time);
+    } else {
+      if (c->rt.advanced_time != 1)
+        error("Called cell with wrong advanced_time counter. Expect=1, got=%d",
+              c->rt.advanced_time);
+    }
+    c->rt.advanced_time = 0;
+#endif
+    return;
+  }
+
+  integertime_t ti_rt_end_min = max_nr_timesteps, ti_rt_beg_max = 0;
+
+  /* Collect the values from the progeny. */
+  for (int k = 0; k < 8; k++) {
+    struct cell *cp = c->progeny[k];
+    if (cp != NULL) {
+
+      /* Recurse */
+      runner_do_collect_rt_times(r, cp, 0);
+
+      /* And update */
+      ti_rt_end_min = min(cp->rt.ti_rt_end_min, ti_rt_end_min);
+      ti_rt_beg_max = max(cp->rt.ti_rt_beg_max, ti_rt_beg_max);
+
+      /* Collected, so clear for next time. */
+      rt_updated += cp->rt.updated;
+      cp->rt.updated = 0;
+    }
+  }
+
+  /* Store the collected values in the cell. */
+  c->rt.ti_rt_end_min = ti_rt_end_min;
+  c->rt.ti_rt_beg_max = ti_rt_beg_max;
+  c->rt.updated = rt_updated;
+
+  if (timer) TIMER_TOC(timer_do_rt_collect_times);
+}
diff --git a/src/scheduler.c b/src/scheduler.c
index b57c2057ad6de20d5ca5051240674de713fa72e2..e714b9e3dffdba1fa20e287dfd53f213df49c915 100644
--- a/src/scheduler.c
+++ b/src/scheduler.c
@@ -1946,6 +1946,7 @@ void scheduler_reweight(struct scheduler *s, int verbose) {
 
     switch (t->type) {
       case task_type_sort:
+      case task_type_rt_sort:
         cost = wscale * intrinsics_popcount(t->flags) * count_i *
                (sizeof(int) * 8 - (count_i ? intrinsics_clz(count_i) : 0));
         break;
@@ -2257,6 +2258,10 @@ void scheduler_reweight(struct scheduler *s, int verbose) {
       case task_type_rt_tchem:
         cost = wscale * count_i;
         break;
+      case task_type_rt_advance_cell_time:
+      case task_type_rt_collect_times:
+        cost = wscale;
+        break;
       case task_type_csds:
         cost =
             wscale * (count_i + gcount_i + scount_i + sink_count_i + bcount_i);
diff --git a/src/space.c b/src/space.c
index 1843a3569ab2e13e5624067498b6870c11cadf39..18d24efb3830e6dc9f9c8294c4572b0bcb31d3cf 100644
--- a/src/space.c
+++ b/src/space.c
@@ -753,101 +753,6 @@ void space_synchronize_particle_positions(struct space *s) {
             clocks_getunit());
 }
 
-void space_convert_rt_star_quantities_after_zeroth_step_mapper(
-    void *restrict map_data, int scount, void *restrict extra_data) {
-#ifdef SWIFT_RT_DEBUG_CHECKS
-  struct spart *restrict sparts = (struct spart *)map_data;
-  const struct engine *restrict e = (struct engine *)extra_data;
-  const int with_cosmology = (e->policy & engine_policy_cosmology);
-
-  for (int k = 0; k < scount; k++) {
-
-    struct spart *restrict sp = &sparts[k];
-    /* Skip extra buffer sparts for on-the-fly creation */
-    if (sparts[k].time_bin > num_time_bins) continue;
-
-    /* get star's age and time step for stellar emission rates */
-    const integertime_t ti_begin =
-        get_integer_time_begin(e->ti_current - 1, sp->time_bin);
-    const integertime_t ti_step = get_integer_timestep(sp->time_bin);
-
-    /* Get particle time-step */
-    double dt_star;
-    if (with_cosmology) {
-      dt_star =
-          cosmology_get_delta_time(e->cosmology, ti_begin, ti_begin + ti_step);
-    } else {
-      dt_star = get_timestep(sp->time_bin, e->time_base);
-    }
-
-    /* Calculate age of the star at current time */
-    const double star_age_end_of_step =
-        stars_compute_age(sp, e->cosmology, e->time, with_cosmology);
-
-    rt_init_star_after_zeroth_step(sp, e->time, star_age_end_of_step, dt_star,
-                                   e->rt_props, e->physical_constants,
-                                   e->internal_units);
-  }
-#endif
-}
-
-void space_convert_rt_hydro_quantities_after_zeroth_step_mapper(
-    void *restrict map_data, int count, void *restrict extra_data) {
-
-#ifdef SWIFT_RT_DEBUG_CHECKS
-  struct part *restrict parts = (struct part *)map_data;
-  const struct engine *restrict e = (struct engine *)extra_data;
-  const struct rt_props *restrict rt_props = e->rt_props;
-  const struct cosmology *restrict cosmo = e->cosmology;
-
-  /* Loop over all the particles ignoring the extra buffer ones for on-the-fly
-   * creation */
-  for (int k = 0; k < count; k++) {
-    struct part *restrict p = &parts[k];
-    if (parts[k].time_bin <= num_time_bins)
-      rt_init_part_after_zeroth_step(p, rt_props, cosmo);
-  }
-#endif
-}
-
-/**
- * @brief Initializes values of radiative transfer data for particles
- * that needs to be set before the first actual step is done, but will
- * be reset in forthcoming steps when the corresponding particle is
- * active. It used to be necessary for a "hydro controlled injection"
- * idea where the star time steps were necessary to be known, now it's
- * only used to set up debugging checks correctly.
- *
- * @param s The #space.
- * @param verbose Are we talkative?
- */
-void space_convert_rt_quantities_after_zeroth_step(struct space *s,
-                                                   int verbose) {
-#ifdef SWIFT_RT_DEBUG_CHECKS
-  const ticks tic = getticks();
-
-  if (s->nr_parts > 0)
-    /* Particle loop. Reset hydro particle values so we don't inject too much
-     * radiation into the gas, and other initialisations after zeroth step. */
-    threadpool_map(&s->e->threadpool,
-                   space_convert_rt_hydro_quantities_after_zeroth_step_mapper,
-                   s->parts, s->nr_parts, sizeof(struct part),
-                   threadpool_auto_chunk_size, /*extra_data=*/s->e);
-
-  if (s->nr_sparts > 0)
-    /* Star particle loop. Hydro controlled injection requires star particles
-     * to have their emission rates computed and ready for interactions. */
-    threadpool_map(&s->e->threadpool,
-                   space_convert_rt_star_quantities_after_zeroth_step_mapper,
-                   s->sparts, s->nr_sparts, sizeof(struct spart),
-                   threadpool_auto_chunk_size, /*extra_data=*/s->e);
-
-  if (verbose)
-    message("took %.3f %s.", clocks_from_ticks(getticks() - tic),
-            clocks_getunit());
-#endif
-}
-
 void space_convert_quantities_mapper(void *restrict map_data, int count,
                                      void *restrict extra_data) {
   struct space *s = (struct space *)extra_data;
diff --git a/src/space.h b/src/space.h
index 57dc24c2cb021a26097e5a8b8ee3fbed9fab2d7e..0c4dcbd6ff0279bbdb24c39b032e0c9d13599c1e 100644
--- a/src/space.h
+++ b/src/space.h
@@ -421,8 +421,6 @@ void space_init_gparts(struct space *s, int verbose);
 void space_init_sparts(struct space *s, int verbose);
 void space_init_bparts(struct space *s, int verbose);
 void space_init_sinks(struct space *s, int verbose);
-void space_convert_rt_quantities_after_zeroth_step(struct space *s,
-                                                   int verbose);
 void space_convert_quantities(struct space *s, int verbose);
 void space_convert_rt_quantities(struct space *s, int verbose);
 void space_link_cleanup(struct space *s);
diff --git a/src/space_recycle.c b/src/space_recycle.c
index 101ba18b836b9d00ad5ead1c4c2cdf51bbc94ea6..71436116740593a719dd461fc6dc0c9cbc6c4094 100644
--- a/src/space_recycle.c
+++ b/src/space_recycle.c
@@ -200,14 +200,24 @@ void space_rebuild_recycle_mapper(void *map_data, int num_elements,
     c->sinks.ti_end_min = -1;
     c->stars.ti_end_min = -1;
     c->black_holes.ti_end_min = -1;
-    c->hydro.rt_in = NULL;
-    c->hydro.rt_ghost1 = NULL;
-    c->hydro.rt_gradient = NULL;
-    c->hydro.rt_ghost2 = NULL;
-    c->hydro.rt_transport = NULL;
-    c->hydro.rt_transport_out = NULL;
-    c->hydro.rt_tchem = NULL;
-    c->hydro.rt_out = NULL;
+    c->rt.rt_in = NULL;
+    c->rt.rt_ghost1 = NULL;
+    c->rt.rt_gradient = NULL;
+    c->rt.rt_ghost2 = NULL;
+    c->rt.rt_transport = NULL;
+    c->rt.rt_transport_out = NULL;
+    c->rt.rt_tchem = NULL;
+    c->rt.rt_advance_cell_time = NULL;
+    c->rt.rt_sorts = NULL;
+    c->rt.rt_out = NULL;
+    c->rt.rt_collect_times = NULL;
+    c->rt.ti_rt_end_min = -1;
+    c->rt.ti_rt_min_step_size = -1;
+    c->rt.updated = 0;
+#ifdef SWIFT_RT_DEBUG_CHECKS
+    c->rt.advanced_time = 0;
+#endif
+
     star_formation_logger_init(&c->stars.sfh);
 #if defined(SWIFT_DEBUG_CHECKS) || defined(SWIFT_CELL_GRAPH)
     c->cellID = 0;
diff --git a/src/space_split.c b/src/space_split.c
index 318e306945306d033d917d1df57be60acee5c47a..74393dd4499f3d012f3e05112999665dc4d950c6 100644
--- a/src/space_split.c
+++ b/src/space_split.c
@@ -75,6 +75,8 @@ void space_split_recursive(struct space *s, struct cell *c,
   float sinks_h_max_active = 0.f;
   integertime_t ti_hydro_end_min = max_nr_timesteps, ti_hydro_end_max = 0,
                 ti_hydro_beg_max = 0;
+  integertime_t ti_rt_end_min = max_nr_timesteps, ti_rt_beg_max = 0;
+  integertime_t ti_rt_min_step_size = max_nr_timesteps;
   integertime_t ti_gravity_end_min = max_nr_timesteps, ti_gravity_end_max = 0,
                 ti_gravity_beg_max = 0;
   integertime_t ti_stars_end_min = max_nr_timesteps, ti_stars_end_max = 0,
@@ -307,6 +309,10 @@ void space_split_recursive(struct space *s, struct cell *c,
 
         ti_hydro_end_min = min(ti_hydro_end_min, cp->hydro.ti_end_min);
         ti_hydro_beg_max = max(ti_hydro_beg_max, cp->hydro.ti_beg_max);
+        ti_rt_end_min = min(ti_rt_end_min, cp->rt.ti_rt_end_min);
+        ti_rt_beg_max = max(ti_rt_beg_max, cp->rt.ti_rt_beg_max);
+        ti_rt_min_step_size =
+            min(ti_rt_min_step_size, cp->rt.ti_rt_min_step_size);
         ti_gravity_end_min = min(ti_gravity_end_min, cp->grav.ti_end_min);
         ti_gravity_beg_max = max(ti_gravity_beg_max, cp->grav.ti_beg_max);
         ti_stars_end_min = min(ti_stars_end_min, cp->stars.ti_end_min);
@@ -468,13 +474,23 @@ void space_split_recursive(struct space *s, struct cell *c,
 
       /* When does this particle's time-step start and end? */
       const timebin_t time_bin = parts[k].time_bin;
+      const timebin_t time_bin_rt = parts[k].rt_time_data.time_bin;
       const integertime_t ti_end = get_integer_time_end(ti_current, time_bin);
       const integertime_t ti_beg = get_integer_time_begin(ti_current, time_bin);
+      const integertime_t ti_rt_end =
+          get_integer_time_end(ti_current, time_bin_rt);
+      const integertime_t ti_rt_beg =
+          get_integer_time_begin(ti_current, time_bin_rt);
+      const integertime_t ti_rt_step = get_integer_timestep(time_bin_rt);
 
       ti_hydro_end_min = min(ti_hydro_end_min, ti_end);
       ti_hydro_end_max = max(ti_hydro_end_max, ti_end);
       ti_hydro_beg_max = max(ti_hydro_beg_max, ti_beg);
 
+      ti_rt_end_min = min(ti_rt_end_min, ti_rt_end);
+      ti_rt_beg_max = max(ti_rt_beg_max, ti_rt_beg);
+      ti_rt_min_step_size = min(ti_rt_min_step_size, ti_rt_step);
+
       h_max = max(h_max, parts[k].h);
 
       if (part_is_active(&parts[k], e))
@@ -635,6 +651,9 @@ void space_split_recursive(struct space *s, struct cell *c,
   c->hydro.h_max_active = h_max_active;
   c->hydro.ti_end_min = ti_hydro_end_min;
   c->hydro.ti_beg_max = ti_hydro_beg_max;
+  c->rt.ti_rt_end_min = ti_rt_end_min;
+  c->rt.ti_rt_beg_max = ti_rt_beg_max;
+  c->rt.ti_rt_min_step_size = ti_rt_min_step_size;
   c->grav.ti_end_min = ti_gravity_end_min;
   c->grav.ti_beg_max = ti_gravity_beg_max;
   c->stars.ti_end_min = ti_stars_end_min;
diff --git a/src/task.c b/src/task.c
index 2ad9d5577b538cffaa56af6f935d1697de8f7111..772276650589770a81b61e56abaeacfb7e66cc86 100644
--- a/src/task.c
+++ b/src/task.c
@@ -122,6 +122,9 @@ const char *taskID_names[task_type_count] = {
     "rt_ghost2",
     "rt_transport_out",
     "rt_tchem",
+    "rt_advance_cell_time",
+    "rt_sorts",
+    "rt_collect_times",
 };
 
 /* Sub-task type names. */
@@ -256,6 +259,7 @@ __attribute__((always_inline)) INLINE static enum task_actions task_acts_on(
     case task_type_rt_ghost1:
     case task_type_rt_ghost2:
     case task_type_rt_tchem:
+    case task_type_rt_sort:
       return task_action_part;
       break;
 
@@ -542,6 +546,8 @@ void task_unlock(struct task *t) {
     case task_type_rt_ghost1:
     case task_type_rt_ghost2:
     case task_type_rt_tchem:
+    case task_type_rt_sort:
+    case task_type_rt_advance_cell_time:
       cell_unlocktree(ci);
       break;
 
@@ -763,6 +769,8 @@ int task_lock(struct task *t) {
     case task_type_rt_ghost1:
     case task_type_rt_ghost2:
     case task_type_rt_tchem:
+    case task_type_rt_sort:
+    case task_type_rt_advance_cell_time:
       if (ci->hydro.hold) return 0;
       if (cell_locktree(ci) != 0) return 0;
       break;
@@ -1780,6 +1788,8 @@ enum task_categories task_get_category(const struct task *t) {
     case task_type_rt_transport_out:
     case task_type_rt_tchem:
     case task_type_rt_out:
+    case task_type_rt_sort:
+    case task_type_rt_advance_cell_time:
       return task_category_rt;
 
     case task_type_neutrino_weight:
diff --git a/src/task.h b/src/task.h
index bdd6110935a5ae37898e6d1422ca2d610f1a7e44..60d51ba706404d8413edd22f9baddc9fbc5cf478 100644
--- a/src/task.h
+++ b/src/task.h
@@ -115,6 +115,9 @@ enum task_types {
   task_type_rt_ghost2,
   task_type_rt_transport_out, /* Implicit */
   task_type_rt_tchem,
+  task_type_rt_advance_cell_time,
+  task_type_rt_sort,
+  task_type_rt_collect_times,
   task_type_count
 } __attribute__((packed));
 
diff --git a/src/timers.c b/src/timers.c
index 4aa493a059270f71882ccc3ef61db42a4e1e6785..2f1e04bf83e5363c7786b379f7d446c9c745942e 100644
--- a/src/timers.c
+++ b/src/timers.c
@@ -140,6 +140,8 @@ const char* timers_names[timer_count] = {
     "dosub_self_rt_transport",
     "dosub_pair_rt_transport",
     "rt_tchem",
+    "rt_advance_cell_time",
+    "rt_collect_times",
 };
 
 /* File to store the timers */
diff --git a/src/timers.h b/src/timers.h
index 341f84c575c219aff6ba3dcff3e0fed2e6cb7a11..ad20f6a414e0b36f8a2118ee28cc8768f6e4fc0a 100644
--- a/src/timers.h
+++ b/src/timers.h
@@ -140,6 +140,8 @@ enum {
   timer_dosub_self_rt_transport,
   timer_dosub_pair_rt_transport,
   timer_do_rt_tchem,
+  timer_do_rt_advance_cell_time,
+  timer_do_rt_collect_times,
   timer_count,
 };
 
diff --git a/src/timestep.h b/src/timestep.h
index 74f30db65fef4cd6cea5933a0f420734352b27a5..57c3d096eccb14dda1c5e436a4d289c6b2e843e6 100644
--- a/src/timestep.h
+++ b/src/timestep.h
@@ -179,10 +179,19 @@ __attribute__((always_inline)) INLINE static integertime_t get_part_timestep(
 
   /* Get the RT timestep */
   float new_dt_radiation = FLT_MAX;
-  if (e->policy & engine_policy_rt)
+  if (e->policy & engine_policy_rt) {
     new_dt_radiation = rt_compute_timestep(
         p, xp, e->rt_props, e->cosmology, e->hydro_properties,
         e->physical_constants, e->internal_units);
+    if (e->max_nr_rt_subcycles > 0 && new_dt_radiation != FLT_MAX) {
+      /* if max_nr_rt_subcycles == 0, we don't subcycle. */
+      /* ToDo: this is a temporary solution to enforce the exact
+       * number of RT subcycles. We multiply the new_dt_rad here by
+       * the number of subcycles, and don't when getting the
+       * actual RT time step. */
+      new_dt_radiation *= e->max_nr_rt_subcycles;
+    }
+  }
 
   /* Take the minimum of all */
   float new_dt = min3(new_dt_hydro, new_dt_cooling, new_dt_grav);
@@ -217,6 +226,28 @@ __attribute__((always_inline)) INLINE static integertime_t get_part_timestep(
   return new_dti;
 }
 
+/**
+ * @brief Compute the new (integer) time-step of a given #part
+ *
+ * @param p The #part.
+ * @param xp The #xpart partner of p.
+ * @param e The #engine (used to get some constants).
+ */
+__attribute__((always_inline)) INLINE static integertime_t get_part_rt_timestep(
+    const struct part *restrict p, const struct xpart *restrict xp,
+    const struct engine *restrict e) {
+
+  integertime_t new_dti;
+  /* TODO: for now, we abuse max_nr_rt_subcycles to fix the
+   * number of sub-cycles for each step. */
+  if (e->max_nr_rt_subcycles > 0) {
+    new_dti = get_part_timestep(p, xp, e) / e->max_nr_rt_subcycles;
+  } else {
+    new_dti = get_part_timestep(p, xp, e);
+  }
+  return new_dti;
+}
+
 /**
  * @brief Compute the new (integer) time-step of a given #spart
  *
diff --git a/tools/timed_functions.py b/tools/timed_functions.py
index ad1cc33e892159aa8cf2d9f58f37912f0984d2f6..d4ad20fae9d95219df6bf573b7154f6b49a985e2 100644
--- a/tools/timed_functions.py
+++ b/tools/timed_functions.py
@@ -37,6 +37,7 @@ labels = [
     ["engine_collect_end_of_step:", 0],
     ["engine_launch: \(tasks\)", 0],
     ["engine_launch: \(timesteps\)", 0],
+    ["engine_launch: \(cycles\)", 0],
     ["writing particle properties", 0],
     ["engine_repartition:", 0],
     ["engine_exchange_cells:", 1],