diff --git a/SAS/DataManagement/Cleanup/CleanupService/service.py b/SAS/DataManagement/Cleanup/CleanupService/service.py
index 4234cff4cbd0fbccb4902fd66c1ea1243e4b098c..f43b5438fb1aa8780ae6719c98b9984caae31bd5 100644
--- a/SAS/DataManagement/Cleanup/CleanupService/service.py
+++ b/SAS/DataManagement/Cleanup/CleanupService/service.py
@@ -482,7 +482,7 @@ class TMSSEventMessageHandlerForCleanup(TMSSEventMessageHandler):
         # when an ingest subtask finishes, then it is safe for the related cleanup subtask(s) to be started
         subtasks = self._tmss_client.get_subtasks_in_same_scheduling_unit(subtask)
         ingest_subtasks = [s for s in subtasks if s['subtask_type'] == 'ingest']
-        unfinished_ingest_subtasks = [s for s in ingest_subtasks if s['state_value'] != 'finished']
+        unfinished_ingest_subtasks = [s for s in ingest_subtasks if s['state_value'] != 'finished' and s['obsolete_since'] is None]
 
         if len(unfinished_ingest_subtasks) > 0:
             logger.info("cleanup subtask id=%s is scheduled, but waiting for ingest id=%s to finish before queueing the cleanup subtask...",
diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py
index 84003a51f427f43e3fedede05550957aded13f8e..5a65a637b02c19b97d9017f2f8176fea7ed9b995 100644
--- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py
+++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py
@@ -40,7 +40,7 @@ from lofar.sas.tmss.tmss.tmssapp.conversions import *
 from lofar.common.util import noop
 from lofar.sas.tmss.tmss.tmssapp import models
 from lofar.sas.tmss.tmss.exceptions import *
-from lofar.sas.tmss.tmss.tmssapp.subtasks import enough_stations_available
+from lofar.sas.tmss.tmss.tmssapp.subtasks import enough_stations_available, get_missing_stations
 from lofar.sas.tmss.tmss.tmssapp.tasks import mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable
 
 import logging
@@ -379,7 +379,7 @@ def sort_scheduling_units_scored_by_constraints(scheduling_units: [models.Schedu
                                                           x.scheduling_unit.created_at), reverse=True)
 
 
-def can_run_within_timewindow(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime, gridder: Gridder=None) -> bool:
+def can_run_within_timewindow(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> bool:
     '''determine if the given scheduling_unit can run withing the given timewindow evaluating all constraints from the "constraints" version 1 template
     :param raise_if_interruped: a callable function which raises under an externally set condition (an 'interrupt' flag was set). This function is/can_be used to interrupt a long-running scheduling call to do an early exit and start a new scheduling call. Default used function is noop (no-operation), thus no interruptable behaviour.
     '''
@@ -387,7 +387,7 @@ def can_run_within_timewindow(scheduling_unit: models.SchedulingUnitBlueprint, l
         gridder = Gridder()
 
     # Seek the earliest_possible_start_time. If existing and within window, then the unit can run within this window
-    earliest_possible_start_time = get_earliest_possible_start_time(scheduling_unit, lower_bound, upper_bound, gridder)
+    earliest_possible_start_time = get_earliest_possible_start_time(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped)
     if earliest_possible_start_time is not None:
         earliest_possible_stop_time = earliest_possible_start_time + scheduling_unit.specified_main_observation_duration
         if earliest_possible_start_time >= lower_bound and earliest_possible_stop_time <= upper_bound:
@@ -994,7 +994,7 @@ def evaluate_sky_min_distance_constraint(scheduling_unit: models.SchedulingUnitB
     return result
 
 @lru_cache(maxsize=10000)
-def get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=Gridder()) -> datetime:
+def get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=Gridder(), raise_if_interruped: Callable=noop) -> datetime:
     # do expensive search from lower_bound until 24 hours later with small steps
     # (sky constrains are (almost) cyclic over 24 hours).
     # first occurrence where min_elevation constraint is met is taken as rough estimate of earliest_possible_start_time
@@ -1004,6 +1004,8 @@ def get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit: mode
         upper_bound = lower_bound + timedelta(hours=24)
     upper_bound = max(lower_bound, upper_bound)
     while possible_start_time < upper_bound:
+        raise_if_interruped()
+
         result = evaluate_sky_min_elevation_constraint(scheduling_unit, possible_start_time, gridder=gridder)
         logger.debug('get_earliest_possible_start_time_for_sky_min_elevation %s', result)
 
@@ -1031,7 +1033,7 @@ def get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit: mode
     return None
 
 
-def get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None) -> datetime:
+def get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> datetime:
     # compute the transit time, and thus the optimal_start_time and earliest_possible_start_time
     if gridder is None:
         gridder = Gridder()
@@ -1044,6 +1046,8 @@ def get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit: mod
 
     allow_quick_jump = True # see below, we can quick jump once, but use monotonous increments so ensure an exit of the while loop.
     while possible_start_time < upper_bound:
+        raise_if_interruped()
+
         gridded_possible_start_time = gridder.grid_time(possible_start_time)
         result = evaluate_sky_transit_constraint(scheduling_unit, gridded_possible_start_time, gridder=gridder, which='next')
         logger.debug('get_earliest_possible_start_time_for_sky_transit_offset %s', result)
@@ -1068,7 +1072,7 @@ def get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit: mod
     return None
 
 @lru_cache(maxsize=10000)
-def get_earliest_possible_start_time_for_sky_min_distance(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None) -> datetime:
+def get_earliest_possible_start_time_for_sky_min_distance(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> datetime:
     # do expensive search from lower_bound until 24 hours later with small steps
     # (sky constrains are (almost) cyclic over 24 hours).
     # first occurrence where min_distance constraint is met is taken as rough estimate of earliest_possible_start_time
@@ -1081,6 +1085,8 @@ def get_earliest_possible_start_time_for_sky_min_distance(scheduling_unit: model
     upper_bound = max(lower_bound, upper_bound)
 
     while possible_start_time < upper_bound:
+        raise_if_interruped()
+
         result = evaluate_sky_min_distance_constraint(scheduling_unit, possible_start_time, gridder=gridder)
         logger.debug('get_earliest_possible_start_time_for_sky_min_distance %s', result)
 
@@ -1247,7 +1253,7 @@ def evaluate_daily_constraints(scheduling_unit: models.SchedulingUnitBlueprint,
     return result
 
 @lru_cache(maxsize=10000)
-def get_earliest_possible_start_time_for_daily_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None) -> datetime:
+def get_earliest_possible_start_time_for_daily_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> datetime:
     # search from lower_bound until 24 hours later with 6 hour steps
     # (daily constrains are (almost) cyclic over 24 hours)
     if gridder is None:
@@ -1255,28 +1261,35 @@ def get_earliest_possible_start_time_for_daily_constraints(scheduling_unit: mode
     gridded_lower_bound = gridder.grid_time(lower_bound)
     possible_start_time = gridded_lower_bound
     while possible_start_time < lower_bound+timedelta(hours=24):
+        raise_if_interruped()
+
         result = evaluate_daily_constraints(scheduling_unit, possible_start_time, gridder=gridder)
         logger.debug('get_earliest_possible_start_time_for_daily_constraints %s', result)
 
         if not result.has_constraint:
             return None
 
-        if result.earliest_possible_start_time is None or result.earliest_possible_start_time < lower_bound:
-            # advance with a grid step, and evaluate again
-            possible_start_time += gridder.as_timedelta()
-            continue
-
         if not result.is_constraint_met and result.earliest_possible_start_time is not None:
             # advance straight to earliest_possible_start_time, and evaluate again to ensure the constraint is met
             possible_start_time = gridder.grid_time(result.earliest_possible_start_time)
             continue
 
         if result.is_constraint_met:
-            return result.earliest_possible_start_time
+            logger.debug('get_earliest_possible_start_time_for_daily_constraints(id=%s, lb=%s, up=%s) result=%s', scheduling_unit.id, lower_bound, upper_bound, result)
+            if result.earliest_possible_start_time >= lower_bound:
+                if upper_bound is None or result.earliest_possible_start_time < upper_bound:
+                    return result.earliest_possible_start_time
+                else:
+                    # do not advance past upper_bound
+                    return None
+
+        # advance with a grid step, and evaluate again
+        possible_start_time += gridder.as_timedelta()
+
     return None
 
 
-def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None) -> datetime:
+def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> datetime:
     '''
     '''
     constraints = scheduling_unit.scheduling_constraints_doc
@@ -1292,10 +1305,11 @@ def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: model
 
         if 'after' in constraints['time']:
             after = parser.parse(constraints['time']['after'], ignoretz=True)
-            if lower_bound is not None:
-                earliest_possible_start_times.add(max(lower_bound, after))
-            else:
-                earliest_possible_start_times.add(after)
+            if upper_bound is None or after <= upper_bound - scheduling_unit.specified_main_observation_duration:
+                if lower_bound is not None:
+                    earliest_possible_start_times.add(max(lower_bound, after))
+                else:
+                    earliest_possible_start_times.add(after)
 
         if 'before' in constraints['time']:
             before = parser.parse(constraints['time']['before'], ignoretz=True)
@@ -1329,6 +1343,10 @@ def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: model
                         earliest_possible_start_times.add(potential_earliest_possible)
                         break
 
+    if not earliest_possible_start_times and not constraints.get('time'):
+        # an empty time constraint means it can just run at/after lower_bound
+        return lower_bound
+
     if lower_bound is not None:
         earliest_possible_start_times = [t for t in earliest_possible_start_times if t >= lower_bound]
 
@@ -1338,7 +1356,7 @@ def get_earliest_possible_start_time_for_time_constraints(scheduling_unit: model
     if earliest_possible_start_times:
         return max(earliest_possible_start_times)
 
-    return lower_bound
+    return None
 
 
 def get_at_constraint_timestamp(scheduling_unit: models.SchedulingUnitBlueprint) -> datetime:
@@ -1348,7 +1366,7 @@ def get_at_constraint_timestamp(scheduling_unit: models.SchedulingUnitBlueprint)
         return at
     return None
 
-def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None) -> datetime:
+def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> datetime:
     '''
     '''
     _method_start_timestamp = datetime.utcnow()
@@ -1372,9 +1390,11 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep
                                                     get_earliest_possible_start_time_for_sky_transit_offset,
                                                     get_earliest_possible_start_time_for_sky_min_distance):
         try:
-            earliest_possible_start_time = get_earliest_possible_start_time_method(scheduling_unit, lower_bound, upper_bound, gridder)
+            earliest_possible_start_time = get_earliest_possible_start_time_method(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped)
             if earliest_possible_start_time is not None:
                 earliest_possible_start_times.add(earliest_possible_start_time)
+        except SchedulerInterruptedException:
+            raise
         except Exception as e:
             logger.exception(e)
 
@@ -1574,7 +1594,7 @@ def compute_scheduling_unit_scores(scheduling_unit: models.SchedulingUnitBluepri
                                 # return the actual (not the gridded) weighted_start_time
                                 start_time=weighted_start_time)
 
-def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound: datetime, upper_bound: datetime=None, raise_if_interruped: Callable=noop, gridder: Gridder=None) -> datetime:
+def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> datetime:
     '''deterimine the earliest possible starttime over all given scheduling units, taking into account all their constraints
     :param raise_if_interruped: a callable function which raises under an externally set condition (an 'interrupt' flag was set). This function is/can_be used to interrupt a long-running scheduling call to do an early exit and start a new scheduling call. Default used function is noop (no-operation), thus no interruptable behaviour.
     '''
@@ -1585,7 +1605,7 @@ def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUni
         raise_if_interruped()
 
         if scheduling_unit.scheduling_constraints_template is not None:
-            earliest_possible_start_time = get_earliest_possible_start_time(scheduling_unit, lower_bound, upper_bound, gridder)
+            earliest_possible_start_time = get_earliest_possible_start_time(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped)
             if earliest_possible_start_time is not None:
                 if min_earliest_possible_start_time is None or earliest_possible_start_time < min_earliest_possible_start_time:
                     min_earliest_possible_start_time = earliest_possible_start_time
@@ -1606,40 +1626,74 @@ def can_run_within_station_reservations(scheduling_unit: models.SchedulingUnitBl
 
     return True
 
-def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime, gridder: Gridder) -> models.SchedulingUnitBlueprint:
+def get_missing_stations_for_scheduling_unit(scheduling_unit: models.SchedulingUnitBlueprint) -> []:
+    observation_subtasks = models.Subtask.independent_subtasks().filter(task_blueprint__scheduling_unit_blueprint_id=scheduling_unit.id).filter(specifications_template__type__value=models.SubtaskType.Choices.OBSERVATION.value).all()
+    missing_stations = set()
+
+    for subtask in observation_subtasks:
+        for station in get_missing_stations(subtask):
+            missing_stations.add(station)
+
+    return sorted((list(missing_stations)))
+
+def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime, gridder: Gridder, raise_if_interruped: Callable=noop) -> models.SchedulingUnitBlueprint:
     try:
+        logger.debug("determine_unschedulable_reason_and_mark_unschedulable_if_needed: scheduling_unit id=%s", scheduling_unit.id)
+        if not can_run_within_station_reservations(scheduling_unit):
+            missing_stations = get_missing_stations_for_scheduling_unit(scheduling_unit)
+            msg = "Stations %s are reserved" % (missing_stations, )
+            return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg)
+
         if gridder is None:
             gridder = Gridder()
-        # this method relies on caching in the used methods in order to be fast.
-        # check if the unit can run at all in the given window
-        if not can_run_within_timewindow(scheduling_unit, lower_bound, upper_bound, gridder):
+
+        if not can_run_within_timewindow(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped):
             # nope, can't run, so check each type of constraint
+            unmet_constraints = []
 
-            at = get_at_constraint_timestamp(scheduling_unit)
-            if at:
-                lower_bound = at
-                upper_bound = at
+            if 'time' in scheduling_unit.scheduling_constraints_doc:
+                if scheduling_unit.scheduling_constraints_doc['time'].get('between', []):
+                    # recurse for each of the 'between' intervals until unschedulable
+                    for between in scheduling_unit.scheduling_constraints_doc['time']['between']:
+                        between_from = parser.parse(between["from"], ignoretz=True)
+                        between_to = parser.parse(between["to"], ignoretz=True)
+                        if between_from != lower_bound or between_to != upper_bound:
+                            scheduling_unit = determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit, between_from, between_to, gridder)
+                            if scheduling_unit.status.value == models.SchedulingUnitStatus.Choices.UNSCHEDULABLE.value:
+                                return scheduling_unit
+
+                # check 'at' constraint
+                at = get_at_constraint_timestamp(scheduling_unit)
+                if at:
+                    if at < lower_bound or at + scheduling_unit.specified_main_observation_duration > upper_bound:
+                        msg = "constraint time.at='%s' falls outside of window ['%s', '%s']" % (round_to_second_precision(at),
+                                                                                                round_to_second_precision(lower_bound),
+                                                                                                round_to_second_precision(upper_bound))
+                        return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg)
+
+                    # use the 'at' timestamp as bounds for the remaining checks below
+                    lower_bound = at
+                    upper_bound = at
 
-            unmet_constraints = []
             if 'sky' in scheduling_unit.scheduling_constraints_doc:
                 if 'min_elevation' in scheduling_unit.scheduling_constraints_doc['sky']:
-                    if get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit, lower_bound, upper_bound, gridder) is None:
-                        unmet_constraints.append("sky min elevation")
+                    if get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None:
+                        unmet_constraints.append("sky min_elevation")
 
                 if 'transit_offset' in scheduling_unit.scheduling_constraints_doc['sky']:
-                    if get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit, lower_bound, upper_bound, gridder) is None:
-                        unmet_constraints.append("sky transit offset")
+                    if get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None:
+                        unmet_constraints.append("sky transit_offset")
 
                 if 'min_distance' in scheduling_unit.scheduling_constraints_doc['sky']:
-                    if get_earliest_possible_start_time_for_sky_min_distance(scheduling_unit, lower_bound, upper_bound, gridder) is None:
-                        unmet_constraints.append("sky min distance")
+                    if get_earliest_possible_start_time_for_sky_min_distance(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None:
+                        unmet_constraints.append("sky min_distance")
 
             if 'time' in scheduling_unit.scheduling_constraints_doc:
-                if get_earliest_possible_start_time_for_time_constraints(scheduling_unit, lower_bound, upper_bound, gridder) is None:
+                if get_earliest_possible_start_time_for_time_constraints(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None:
                     unmet_constraints.append("time")
 
             if 'daily' in scheduling_unit.scheduling_constraints_doc:
-                if get_earliest_possible_start_time_for_daily_constraints(scheduling_unit, lower_bound, upper_bound, gridder) is None:
+                if get_earliest_possible_start_time_for_daily_constraints(scheduling_unit, lower_bound, upper_bound, gridder, raise_if_interruped) is None:
                     unmet_constraints.append("daily")
 
             if unmet_constraints:
@@ -1649,10 +1703,13 @@ def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_u
             else:
                 mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, "sorry, unknown unschedulable reason.")
 
+    except SchedulerInterruptedException:
+        raise
     except Exception as e:
         logger.exception(e)
         mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, str(e))
 
+    scheduling_unit.refresh_from_db()
     return scheduling_unit
 
 
diff --git a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py
index 3cf7383f932d42d00476c4c7275ddc57ce0cad43..831746b088bc57b819e627cbe62ea6e1cdec4b4c 100644
--- a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py
+++ b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py
@@ -257,13 +257,13 @@ class Scheduler:
                                 logger.error(e)
                                 mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(schedulable_unit, reason=str(e))
                         else:
-                            msg = "fixed_time-scheduled scheduling unit id=%d cannot be scheduled at '%s'" % (schedulable_unit.id, start_time)
-                            logger.warning(msg)
-                            mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(schedulable_unit, reason=msg)
+                            logger.warning("fixed_time-scheduled scheduling unit id=%d cannot be scheduled at '%s'", schedulable_unit.id, start_time)
+                            determine_unschedulable_reason_and_mark_unschedulable_if_needed(schedulable_unit, start_time, start_time)
 
                         self.log_schedule(log_level=logging.DEBUG)
                     except Exception as e:
                         logger.exception("Could not schedule fixed_time-scheduled scheduling unit id=%d: %s", schedulable_unit.id, e)
+                        mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(schedulable_unit, reason=str(e))
             else:
                 logger.info("there are no schedulable scheduling units with fixed_time at constraint for active projects to schedule")
 
@@ -342,7 +342,7 @@ class Scheduler:
         logger.info("find_best_next_schedulable_unit: units meeting constraints in window ['%s', '%s']: %s", lower_bound_start_time, upper_bound_stop_time, ','.join([str(su.id) for su in sorted(filtered_scheduling_units, key=lambda x: x.id)]) or 'None')
 
         if not filtered_scheduling_units:
-            logger.warning("find_best_next_schedulable_unit: no units meeting constraints in window ['%s', '%s']", lower_bound_start_time, upper_bound_stop_time)
+            logger.info("find_best_next_schedulable_unit: no units meeting constraints in window ['%s', '%s']", lower_bound_start_time, upper_bound_stop_time)
             return None
 
         # then, check if there is a subset that can only run exclusively in this window and not later.
@@ -484,14 +484,14 @@ class Scheduler:
                 # nothing was found, or an error occurred.
                 # it may be that in the mean time some scheduling_units are not (dynamically) schedulable anymore, filter those out.
                 for su in candidate_units:
-                    determine_unschedulable_reason_and_mark_unschedulable_if_needed(su, lower_bound_start_time, upper_bound_stop_time, self.search_gridder)
+                    determine_unschedulable_reason_and_mark_unschedulable_if_needed(su, lower_bound_start_time, upper_bound_stop_time, self.search_gridder, raise_if_interruped=self._raise_if_triggered)
 
                 # all units are refreshed and either schedulable or unschedulable.
                 # refresh list of schedulable_units to be considered in next round (only schedulable)
                 candidate_units = [su for su in candidate_units if su.status.value==models.SchedulingUnitStatus.Choices.SCHEDULABLE.value]
 
                 # advance the window
-                min_earliest_possible_start_time = get_min_earliest_possible_start_time(candidate_units, lower_bound_start_time+timedelta(hours=1), lower_bound_start_time+timedelta(hours=25), self._raise_if_triggered, gridder=self.search_gridder)
+                min_earliest_possible_start_time = get_min_earliest_possible_start_time(candidate_units, lower_bound_start_time+timedelta(hours=1), lower_bound_start_time+timedelta(hours=25), gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered)
                 if min_earliest_possible_start_time is None:
                     lower_bound_start_time += timedelta(hours=6)
                 else:
@@ -620,7 +620,7 @@ class Scheduler:
                             scheduling_units.remove(placed_B_unit)
                 else:
                     # search again in a later timeslot
-                    min_earliest_possible_start_time = get_min_earliest_possible_start_time(scheduling_units, lower_bound_start_time+timedelta(minutes=60), lower_bound_start_time+timedelta(hours=25), self._raise_if_triggered, gridder=self.search_gridder)
+                    min_earliest_possible_start_time = get_min_earliest_possible_start_time(scheduling_units, lower_bound_start_time+timedelta(minutes=60), lower_bound_start_time+timedelta(hours=25), gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered)
                     logger.info("lower_bound_start_time='%s', min_earliest_possible_start_time='%s'", lower_bound_start_time, min_earliest_possible_start_time)
                     if min_earliest_possible_start_time is not None and min_earliest_possible_start_time > lower_bound_start_time:
                         lower_bound_start_time = min_earliest_possible_start_time
diff --git a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py
index e00056c44dcb6152e5bcca55500ce2d8374cdea2..c9bd0d7660056b7a62087d4ea0bceb1c9e17d649 100755
--- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py
+++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py
@@ -98,7 +98,8 @@ class BaseDynamicSchedulingTestCase(unittest.TestCase):
         weight_factor.weight = 0
         weight_factor.save()
 
-    def clean_environment(self):
+    @staticmethod
+    def clean_environment():
         # wipe all scheduling_unit_drafts in between tests, so the tests don't influence each other
         tmss_test_env.delete_scheduling_unit_drafts_cascade()
         models.Reservation.objects.all().delete()
@@ -1508,6 +1509,58 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase):
         # this test passes when there are no failed templates
         self.assertTrue(len(failed_templates)==0, msg='failed_templates: %s' % ([(t.name, t.version) for t in failed_templates]))
 
+    def test_unschedulable_reasons_due_to_unmet_constraints(self):
+        """
+        Test if the correct unschedulable_reason is set when a constraint is not met.
+        """
+        # use a short cycle, far in the future, for which we known how the target behaves in this period (transit, elevation, etc)
+        cycle = models.Cycle.objects.create(**Cycle_test_data(start=datetime(2030, 1, 1), stop=datetime(2030, 1, 7)))
+        project = models.Project.objects.create(**Project_test_data(name=str(uuid.uuid4()), project_state=models.ProjectState.objects.get(value=models.ProjectState.Choices.ACTIVE.value)))
+        project.cycles.add(cycle)
+        project.save()
+        scheduling_set = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project))
+        scheduling_unit_draft = self.create_simple_observation_scheduling_unit(str(uuid.uuid4()), scheduling_set=scheduling_set, obs_duration=3600)
+
+        # keep matters simple, use one station
+        obs_task_draft = scheduling_unit_draft.task_drafts.first()
+        obs_task_draft.specifications_doc['station_configuration']['station_groups'][0]['stations'] = ['CS002']
+        obs_task_draft.save()
+
+        scheduling_unit_blueprint = create_scheduling_unit_blueprint_and_tasks_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft)
+
+        # set density_vs_optimal to 1, meaning "as close to transit as possible"
+        weight_factor, created = models.SchedulingConstraintsWeightFactor.objects.get_or_create(scheduling_constraints_template=models.SchedulingConstraintsTemplate.get_latest(name="constraints"), constraint_name="density_vs_optimal")
+        weight_factor.weight = 0
+        weight_factor.save()
+
+        for expected_reason, constraints in (("constraint time.at='2029-12-31 00:00:00' falls outside of window ['2030-01-01 00:00:00', '2030-01-08 00:00:00']",  {'time': {'at': (cycle.start - timedelta(hours=24)).isoformat()}}),
+                                             ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00",  {'time': {'before': (cycle.start - timedelta(hours=24)).isoformat()}}),
+                                             ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00",  {'time': {'after': (cycle.stop + timedelta(hours=24)).isoformat()}}),
+                                             ("sky min_elevation constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00",  {'sky': {'min_elevation': {'target': 1.57}}}),
+                                             ("sky min_distance constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00",  {'sky': {'min_distance': {'sun': 1.57}}}),
+                                             ("sky transit_offset constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00",  {'sky': {'transit_offset': {'from': -900, 'to': 900}}, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}),
+                                             ("daily constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00",  {'daily': {'require_day': True }, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}),
+                                             ("daily constraint is not met anywhere between 2030-01-01 11:00:00 and 2030-01-01 15:00:00",  {'daily': {'require_night': True }, 'time': {'between': [{'from': '2030-01-01T11:00:00Z', 'to': '2030-01-01T15:00:00Z'}]}}),
+                                             ("daily constraint is not met anywhere between 2030-01-01 07:00:00 and 2030-01-01 09:00:00",  {'daily': {'avoid_twilight': True }, 'time': {'between': [{'from': '2030-01-01T07:00:00Z', 'to': '2030-01-01T09:00:00Z'}]}}),
+                                             ):
+            # reset unit...
+            mark_independent_subtasks_in_scheduling_unit_blueprint_as_schedulable(scheduling_unit_blueprint)
+            self.assertEqual('', scheduling_unit_blueprint.unschedulable_reason)
+            wipe_evaluate_constraints_caches()
+
+            # set the constraints
+            scheduling_unit_blueprint.scheduling_constraints_doc = {'scheduler': 'dynamic', **constraints}
+            scheduling_unit_blueprint.save()
+
+            # try to schedule, should fail.
+            scheduled_scheduling_units = self.scheduler.do_dynamic_schedule()
+            self.assertEqual(0, len(scheduled_scheduling_units))
+
+            # Assert the scheduling_unit has not been scheduled and that it has the correct expected unschedulable_reason
+            scheduling_unit_blueprint.refresh_from_db()
+            self.assertEqual('unschedulable', scheduling_unit_blueprint.status.value)
+            self.assertEqual(expected_reason, scheduling_unit_blueprint.unschedulable_reason)
+
 
 class TestReservedStationsTimeWindows(BaseDynamicSchedulingTestCase):
     """
@@ -1525,6 +1578,8 @@ class TestReservedStationsTimeWindows(BaseDynamicSchedulingTestCase):
     """
     @classmethod
     def setUpClass(cls) -> None:
+        cls.clean_environment()
+
         super().setUpClass()
 
         # create a three re-usable variants scheduling_unit_blueprint, based on the "IM HBA - 1 Beam" strategy
@@ -1830,6 +1885,37 @@ class TestReservedStationsTimeWindows(BaseDynamicSchedulingTestCase):
                                         start_time=self.scheduling_unit_blueprint.scheduled_start_time)
         self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint))
 
+    def test_unschedulable_reason_when_reserved_stations_block_unit(self):
+        """
+        Test station reservation when 2 station (CS001,CS002) are reserved and stations (CS001, CS002) are used in scheduling_unit
+        """
+        reservation_two = self.create_station_reservation("Two", ["CS001", "CS002"])
+        # reservation start_time > SUB start_time and reservation stop_time > SUB stop_time
+        self.set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(reservation_two, self.scheduling_unit_blueprint_cs001_cs002)
+
+        # set the constraints for scheduling_unit_blueprint_cs001_cs002
+        self.scheduling_unit_blueprint_cs001_cs002.scheduling_constraints_doc['scheduler'] = 'dynamic'
+        self.scheduling_unit_blueprint_cs001_cs002.scheduling_constraints_doc['time'] = {
+            'between': [ {"from": reservation_two.start_time.isoformat(),
+                          "to": reservation_two.stop_time.isoformat()} ]}
+        self.scheduling_unit_blueprint_cs001_cs002.save()
+
+        # mark the other units as fixed time, so they won't interfere
+        self.scheduling_unit_blueprint.scheduling_constraints_doc = {'scheduler': 'fixed_time', 'time': {'at': (reservation_two.stop_time + timedelta(days=1)).isoformat()}}
+        self.scheduling_unit_blueprint.save()
+        self.scheduling_unit_blueprint_cs001.scheduling_constraints_doc = {'scheduler': 'fixed_time', 'time': {'at': (reservation_two.stop_time + timedelta(days=1)).isoformat()}}
+        self.scheduling_unit_blueprint_cs001.save()
+
+        # try to schedule, should fail.
+        wipe_evaluate_constraints_caches()
+        scheduled_scheduling_units = self.scheduler.do_dynamic_schedule()
+        self.assertEqual(0, len(scheduled_scheduling_units))
+
+        # Assert the scheduling_unit has not been scheduled and that it has the correct expected unschedulable_reason
+        self.scheduling_unit_blueprint_cs001_cs002.refresh_from_db()
+        self.assertEqual('unschedulable', self.scheduling_unit_blueprint_cs001_cs002.status.value)
+        self.assertEqual("Stations ['CS001', 'CS002'] are reserved", self.scheduling_unit_blueprint_cs001_cs002.unschedulable_reason)
+
 
 class TestTriggers(BaseDynamicSchedulingTestCase):
     """
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py
index 6a4891a87c2e13276033563c897b9bf929247316..615319ff50700e5b11ac846e6643814bf4e10978 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py
@@ -1202,6 +1202,7 @@ def mark_independent_subtasks_in_task_blueprint_as_schedulable(task_blueprint: T
 def mark_subtask_as_unschedulable(subtask: Subtask, reason: str):
     '''Convenience method: Mark the subtask as unschedulable. Unschedules first if needed.'''
     with transaction.atomic():
+        logger.info("marking subtask id=%s from scheduling_unit_id=%s as unschedulable. reason: %s", subtask.id, subtask.task_blueprint.scheduling_unit_blueprint.id, reason)
         if subtask.state.value == SubtaskState.Choices.SCHEDULED.value:
             unschedule_subtask(subtask, post_state=SubtaskState.objects.get(value=SubtaskState.Choices.UNSCHEDULABLE.value))
         else:
@@ -1223,6 +1224,7 @@ def mark_subtasks_and_successors_as_unschedulable(subtask: Subtask, reason: str)
 def mark_subtask_as_defined(subtask: Subtask):
     '''Convenience method: Mark the subtask as defined, making it's task & scheduling_unit schedulable. Unschedules first if needed.'''
     with transaction.atomic():
+        logger.info("marking subtask id=%s from scheduling_unit_id=%s as defined/schedulable.", subtask.id, subtask.task_blueprint.scheduling_unit_blueprint.id)
         if subtask.state.value == SubtaskState.Choices.SCHEDULED.value:
             unschedule_subtask(subtask, post_state=SubtaskState.objects.get(value=SubtaskState.Choices.DEFINED.value))
         else:
@@ -1736,7 +1738,7 @@ def convert_task_station_groups_specification_to_station_list_without_used_and_o
         available_stations = requested_stations - unavailable_stations
         missing_stations = requested_stations - available_stations
         max_nr_missing = station_group.get('max_nr_missing', 0)
-        if len(missing_stations) > max_nr_missing:
+        if raise_when_too_many_missing and len(missing_stations) > max_nr_missing:
             # early exit. No need to evaluate more groups when one groups does not meet the requirements
             raise TooManyStationsUnavailableException('Subtask id=%s is missing more than max_nr_missing=%s stations which are available between \'%s\' and \'%s\'\nunavailable=%s\nrequested=%s\navailable=%s' % (
                                                       subtask.id, max_nr_missing,
@@ -1771,15 +1773,14 @@ def get_missing_stations(subtask: Subtask) -> []:
     if subtask.specifications_template.type.value != SubtaskType.Choices.OBSERVATION.value:
         return []
 
-    # the observation has to be scheduled or "further"
-    if subtask.state.value in (SubtaskState.Choices.DEFINED.value, SubtaskState.Choices.SCHEDULING.value):
-        return []
-
     # fetch the requested stations from the spec (without removing the unavailable ones! and not raising!)
     requested_stations = set(convert_task_station_groups_specification_to_station_list_without_used_and_or_reserved_stations(subtask, remove_reserved_stations=False, remove_used_stations=False, raise_when_too_many_missing=False))
 
-    # fetch the used_stations from the subtask spec
-    used_stations = set(subtask.specifications_doc.get('stations', {}).get('station_list', []))
+    # fetch the used_stations from the subtask spec, depends on if it's scheduled-and-further
+    if subtask.state.value in (SubtaskState.Choices.DEFINED.value, SubtaskState.Choices.SCHEDULING.value):
+        used_stations = set(convert_task_station_groups_specification_to_station_list_without_used_and_or_reserved_stations(subtask, remove_reserved_stations=True, remove_used_stations=True, raise_when_too_many_missing=False))
+    else:
+        used_stations = set(subtask.specifications_doc.get('stations', {}).get('station_list', []))
 
     # missing is the difference
     return sorted(list(requested_stations-used_stations))