diff --git a/biscuit/apps/chronos/migrations/0010_auto_20190825_1238.py b/biscuit/apps/chronos/migrations/0010_auto_20190825_1238.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9d46c50890ea498b10541b1946db593e5226775
--- /dev/null
+++ b/biscuit/apps/chronos/migrations/0010_auto_20190825_1238.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.1 on 2019-08-25 10:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('timetable', '0009_hint_classes_formatted'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='hintclass',
+            name='class_id',
+            field=models.IntegerField(),
+        ),
+    ]
diff --git a/biscuit/apps/chronos/migrations/0011_merge_20191113_1655.py b/biscuit/apps/chronos/migrations/0011_merge_20191113_1655.py
new file mode 100644
index 0000000000000000000000000000000000000000..91d52bea14baca8a9ac6fa7a49754cc7836cc50e
--- /dev/null
+++ b/biscuit/apps/chronos/migrations/0011_merge_20191113_1655.py
@@ -0,0 +1,13 @@
+# Generated by Django 2.2.6 on 2019-11-13 15:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('timetable', '0010_auto_20190818_0910'),
+        ('timetable', '0010_auto_20190825_1238'),
+    ]
+
+    operations = [
+    ]
diff --git a/biscuit/apps/chronos/pdf.py b/biscuit/apps/chronos/pdf.py
index 08f7dc3d0b010c23ee3b02c2d58c4182eebb96f1..4bc3902eac9e9dbd5e399f484ecc190a1656a232 100644
--- a/biscuit/apps/chronos/pdf.py
+++ b/biscuit/apps/chronos/pdf.py
@@ -1,12 +1,12 @@
 import os
 import subprocess
+import inspect # delete this line
 
 from django.template.loader import render_to_string
 
 from schoolapps.settings import BASE_DIR
 from debug.models import register_log_with_filename
 
-
 LOGO_FILENAME = os.path.join(BASE_DIR, "static", "common", "logo.png")
 
 
diff --git a/biscuit/apps/chronos/urls.py b/biscuit/apps/chronos/urls.py
index 8018e9a9d7387a8a5d4ad7f696cd8605e3a77c71..1a8f979a2854425a642fe53b460ef7db7dd60d99 100755
--- a/biscuit/apps/chronos/urls.py
+++ b/biscuit/apps/chronos/urls.py
@@ -1,3 +1,4 @@
+from django.db import ProgrammingError, OperationalError
 from django.urls import path
 
 from untisconnect.models import Terms, Schoolyear
@@ -26,7 +27,7 @@ try:
         path('<str:plan_date>-aktuell.pdf', views.sub_pdf, name="timetable_substitutions_pdf_date")
     ]
 
-except (Terms.DoesNotExist, Schoolyear.DoesNotExist):
+except (Terms.DoesNotExist, Schoolyear.DoesNotExist, ProgrammingError, OperationalError):
     from . import fallback_view
 
     urlpatterns = [
diff --git a/biscuit/apps/chronos/views.py b/biscuit/apps/chronos/views.py
index 5f9dec41286fd1b928c3f63318533793761b06d7..4d74222767e7370fe83fe2ea3bfa5358c55b421e 100755
--- a/biscuit/apps/chronos/views.py
+++ b/biscuit/apps/chronos/views.py
@@ -1,12 +1,17 @@
 import datetime
 import os
+import time
+import traceback
+from typing import List
 
 from PyPDF2 import PdfFileMerger
 from django.contrib.auth.decorators import login_required, permission_required
 from django.http import Http404, FileResponse
 from django.shortcuts import render, redirect, get_object_or_404
 from django.utils import timezone
+from django.views.decorators.cache import cache_page
 
+from dashboard.caches import SUBS_VIEW_CACHE, MY_PLAN_VIEW_CACHE, PLAN_VIEW_CACHE
 from debug.models import register_traceback, register_return_0
 from schoolapps.settings import BASE_DIR
 from schoolapps.settings import SHORT_WEEK_DAYS, LONG_WEEK_DAYS
@@ -18,6 +23,9 @@ from untisconnect.utils import get_type_and_object_of_user, overview_dict
 from timetable.hints import get_all_hints_by_time_period, get_all_hints_by_class_and_time_period, \
     get_all_hints_for_teachers_by_time_period, get_all_hints_not_for_teachers_by_time_period
 from timetable.pdf import generate_class_tex, generate_pdf
+
+from untisconnect.plan import get_plan, TYPE_TEACHER, TYPE_CLASS, TYPE_ROOM, parse_lesson_times
+from untisconnect.sub import get_substitutions_by_date, generate_sub_table, get_header_information, SubRow
 from untisconnect.api import *
 from untisconnect.events import get_all_events_by_date
 from untisconnect.plan import get_plan, parse_lesson_times
@@ -61,6 +69,7 @@ def quicklaunch(request):
 
 @login_required
 @permission_required("timetable.show_plan")
+@cache_page(PLAN_VIEW_CACHE.expiration_time)
 def plan(request, plan_type, plan_id, regular="", year=None, calendar_week=None):
     """
     [DJANGO VIEW]
@@ -145,6 +154,7 @@ def plan(request, plan_type, plan_id, regular="", year=None, calendar_week=None)
 
 @login_required
 @permission_required("timetable.show_plan")
+@cache_page(MY_PLAN_VIEW_CACHE.expiration_time)
 def my_plan(request, year=None, month=None, day=None):
     date, time = find_out_what_is_today(year, month, day)
 
@@ -170,7 +180,6 @@ def my_plan(request, year=None, month=None, day=None):
 
     elif _type == TYPE_CLASS:
         # Student
-
         plan_id = el.id
         raw_type = "class"
 
@@ -213,6 +222,55 @@ def my_plan(request, year=None, month=None, day=None):
 # SUBSTITUTIONS #
 #################
 
+# TODO: Move to own helper file later
+def equal(sub_row_1: SubRow, sub_row_2: SubRow) -> bool:
+    """
+    Checks the equality of two sub rows
+
+    :param sub_row_1: SubRow 1
+    :param sub_row_2: SubRow 2
+    :return: Equality
+    """
+    return sub_row_1.classes == sub_row_2.classes and sub_row_1.sub and sub_row_2.sub and \
+           sub_row_1.sub.teacher_old == sub_row_2.sub.teacher_old and \
+           sub_row_1.sub.teacher_new == sub_row_2.sub.teacher_new and \
+           sub_row_1.sub.subject_old == sub_row_2.sub.subject_old and \
+           sub_row_1.sub.subject_new == sub_row_2.sub.subject_new and \
+           sub_row_1.sub.room_old == sub_row_2.sub.room_old and \
+           sub_row_1.sub.room_new == sub_row_2.sub.room_new and \
+           sub_row_1.sub.text == sub_row_2.sub.text
+
+
+def merge_sub_rows(sub_table: List[SubRow]) -> List[SubRow]:
+    """
+    Merge equal sub rows with different lesson numbers to one
+
+    :param sub_table:
+    :return:
+    """
+    new_sub_table = []
+    i = 0
+    while i < len(sub_table) - 1:
+        j = 1
+
+        while equal(sub_table[i], sub_table[i + j]):
+            j += 1
+            if i + j > len(sub_table) - 1:
+                break
+        if j > 1:
+            new_sub_row = sub_table[i]
+            new_sub_row.lesson = sub_table[i].lesson + '-' + sub_table[i + j - 1].lesson
+            new_sub_table.append(new_sub_row)
+        else:
+            new_sub_table.append(sub_table[i])
+            # get last item
+            if i == len(sub_table) - 2:
+                new_sub_table.append(sub_table[i + 1])
+                break
+        i += j
+    return new_sub_table
+
+
 def sub_pdf(request, plan_date=None):
     """Show substitutions as PDF for the next weekday (specially for monitors)"""
 
@@ -236,6 +294,7 @@ def sub_pdf(request, plan_date=None):
         subs = get_substitutions_by_date(date)
 
         sub_table = generate_sub_table(subs, events)
+        sub_table = merge_sub_rows(sub_table)
 
         # Get header information and hints
         header_info = get_header_information(subs, date, events)
@@ -275,6 +334,7 @@ def sub_pdf(request, plan_date=None):
 
 @login_required
 @permission_required("timetable.show_plan")
+@cache_page(SUBS_VIEW_CACHE.expiration_time)
 def substitutions(request, year=None, month=None, day=None):
     """Show substitutions in a classic view"""
 
@@ -291,6 +351,9 @@ def substitutions(request, year=None, month=None, day=None):
 
     sub_table = generate_sub_table(subs, events)
 
+    # Merge Subs
+    sub_table = merge_sub_rows(sub_table)
+
     # Get header information and hints
     header_info = get_header_information(subs, date, events)
     hints = list(get_all_hints_by_time_period(date, date))