diff --git a/Dockerfile b/Dockerfile
index 030717cdd793a0a7367cade822888bf852029aff..a16be4644e4aa73edef391ca20b95acf9cf654bd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.8.1
+FROM python:3.11
 
 ENV PYTHONDONTWRITEBYTECODE 1
 ENV PYTHONUNBUFFERED 1
@@ -13,7 +13,6 @@ WORKDIR $APP_HOME
 
 COPY ./requirements/production.txt requirements.txt
 
-RUN apt-get -y update && apt-get install -y python python-pip python-dev python-django-extensions postgresql-client netcat
 RUN pip install -r requirements.txt
 
 COPY ./src $APP_HOME
diff --git a/k8s/deployment.yml b/k8s/deployment.yml
index b979cc6878ed625e3d8ed86a31c45ace5a7070a5..513978576f27f859b3f19b0ed21c936ee168b094 100644
--- a/k8s/deployment.yml
+++ b/k8s/deployment.yml
@@ -107,6 +107,22 @@ spec:
             limits:
               memory: 200Mi
               cpu: "2"
+        - name: worker
+          image: harbor.sch.bme.hu/kszk/kszkepzes-backend:##IMAGETAG##
+          imagePullPolicy: "IfNotPresent"
+          envFrom:
+            - configMapRef:
+                name: kszkepzes-config
+            - secretRef:
+                name: kszkepzes-secret-config
+          command: ["python3"]
+          args: ["-m", "celery", "-A", "kszkepzes", "worker", "-l", "info"]
+          resources:
+            requests:
+              cpu: "100m"
+            limits:
+              memory: 600Mi
+              cpu: "2"
       volumes:
         - name: kszkepzes-media-volume
           persistentVolumeClaim:
diff --git a/k8s/redis.values.yaml b/k8s/redis.values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8791d6943ec3e220dee398fb881f4571a5ff9824
--- /dev/null
+++ b/k8s/redis.values.yaml
@@ -0,0 +1,11 @@
+# https://artifacthub.io/packages/helm/bitnami/redis
+master:
+  resources:
+    limits:
+      memory: 2Gi
+      cpu: 2
+replica:
+  resources:
+    limits:
+      memory: 2Gi
+      cpu: 2
diff --git a/requirements/base.in b/requirements/base.in
index 962a298f6a18f7690943f1df7e8af0c61a963e5a..b9837710c0a62685aba81dca1dc0c7ba90a60f7a 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -1,4 +1,5 @@
-Django==2.2.4
-djangorestframework==3.10.2
-django-solo==1.1.3
-django-import-export==1.2.0
+Django~=4.1
+djangorestframework~=3.14
+django-solo~=2.0
+django-import-export~=3.1
+celery~=5.2
diff --git a/requirements/production.in b/requirements/production.in
index 2ef446c78ccb7cf01c48ff5cee36a04d5f6f6abc..5fa215f62e6cfdc67d77ce8aae0f3f086e83c8c3 100644
--- a/requirements/production.in
+++ b/requirements/production.in
@@ -1,11 +1,13 @@
 -r base.in
 psycopg2-binary
-gunicorn==19.7.1
-flake8==3.7.8
-pip-tools==4.1.0
-django-extensions==2.2.1
-python-language-server==0.28.2
-drf-yasg==1.16.1
-packaging==19.1
-Pillow==7.0.0
-djangorestframework-api-key==1.4.1
\ No newline at end of file
+redis
+gunicorn~=20.1
+flake8~=6.0
+pip-tools~=6.12
+django-extensions~=3.2
+python-language-server~=0.36
+drf-yasg~=1.21
+packaging~=23.0
+Pillow~=9.4
+djangorestframework-api-key~=2.3
+
diff --git a/requirements/production.txt b/requirements/production.txt
index 7cdc80bd945fd1c0c126cfee9d379ae6c4c884f9..75af8c7b7b9dd2cbaf14c87f61a0ca1862d1c6e8 100644
--- a/requirements/production.txt
+++ b/requirements/production.txt
@@ -1,70 +1,170 @@
 #
-# This file is autogenerated by pip-compile
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
 #
 #    pip-compile --output-file=requirements/production.txt requirements/production.in
 #
-attrs==19.3.0             # via packaging
-certifi==2018.1.18        # via requests
-chardet==3.0.4            # via requests
-click==7.0                # via pip-tools
-coreapi==2.3.3            # via drf-yasg
-coreschema==0.0.4         # via coreapi, drf-yasg
-defusedxml==0.5.0         # via python3-openid, social-auth-core
-diff-match-patch==20121119  # via django-import-export
-django-extensions==2.2.1
-django-import-export==1.2.0
-django-social-authsch==0.1
-django-solo==1.1.3
-django==2.2.4
-djangorestframework-api-key==1.4.1
-djangorestframework==3.10.2
-drf-yasg==1.16.1
-entrypoints==0.3          # via flake8
-et-xmlfile==1.0.1         # via openpyxl
-flake8==3.7.8
-future==0.18.2            # via python-language-server
-gunicorn==19.7.1
-idna==2.6                 # via requests
-importlib-metadata==1.5.0  # via pluggy
-inflection==0.3.1         # via drf-yasg
-itypes==1.1.0             # via coreapi
-jdcal==1.3                # via openpyxl
-jedi==0.14.1              # via python-language-server
-jinja2==2.11.1            # via coreschema
-markupsafe==1.1.1         # via jinja2
-mccabe==0.6.1             # via flake8
-oauthlib==2.0.6           # via requests-oauthlib, social-auth-core
-odfpy==1.3.6              # via tablib
-openpyxl==2.5.0           # via tablib
-packaging==19.1
-parso==0.6.1              # via jedi
-pillow==7.0.0
-pip-tools==4.1.0
-pluggy==0.13.1            # via python-language-server
-psycopg2-binary==2.8.4
-pycodestyle==2.5.0        # via flake8
-pyflakes==2.1.1           # via flake8
-pyjwt==1.5.3              # via social-auth-core
-pyparsing==2.4.6          # via packaging
-python-jsonrpc-server==0.3.4  # via python-language-server
-python-language-server==0.28.2
-python3-openid==3.1.0     # via social-auth-core
-pytz==2017.3              # via django
-pyyaml==5.3               # via tablib
-requests-oauthlib==0.8.0  # via social-auth-core
-requests==2.22.0          # via coreapi, requests-oauthlib, social-auth-core
-ruamel.yaml.clib==0.2.0   # via ruamel.yaml
-ruamel.yaml==0.16.7       # via drf-yasg
-six==1.11.0               # via django-extensions, drf-yasg, packaging, pip-tools, social-auth-app-django, social-auth-core
-social-auth-app-django==2.1.0  # via django-social-authsch
-social-auth-core==1.6.0   # via django-social-authsch, social-auth-app-django
-sqlparse==0.3.0           # via django
-tablib==0.12.1            # via django-import-export
-ujson==1.35               # via python-jsonrpc-server
-unicodecsv==0.14.1        # via tablib
-uritemplate==3.0.1        # via coreapi, drf-yasg
-urllib3==1.25.8           # via requests
-xlrd==1.1.0               # via tablib
-xlwt==1.3.0               # via tablib
-zipp==2.1.0               # via importlib-metadata
+amqp==5.1.1
+    # via kombu
+asgiref==3.6.0
+    # via django
+billiard==3.6.4.0
+    # via celery
+build==0.10.0
+    # via pip-tools
+celery==5.2.7
+    # via -r requirements/production.in
+certifi==2018.1.18
+    # via requests
+chardet==3.0.4
+    # via requests
+click==8.1.3
+    # via
+    #   celery
+    #   click-didyoumean
+    #   click-plugins
+    #   click-repl
+    #   pip-tools
+click-didyoumean==0.3.0
+    # via celery
+click-plugins==1.1.1
+    # via celery
+click-repl==0.2.0
+    # via celery
+coreapi==2.3.3
+    # via drf-yasg
+coreschema==0.0.4
+    # via
+    #   coreapi
+    #   drf-yasg
+diff-match-patch==20121119
+    # via django-import-export
+django==4.1.7
+    # via
+    #   -r requirements/base.in
+    #   django-extensions
+    #   django-import-export
+    #   django-solo
+    #   djangorestframework
+    #   drf-yasg
+django-extensions==3.2.1
+    # via -r requirements/production.in
+django-import-export==3.1.0
+    # via -r requirements/base.in
+django-solo==2.0.0
+    # via -r requirements/base.in
+djangorestframework==3.14.0
+    # via
+    #   -r requirements/base.in
+    #   drf-yasg
+djangorestframework-api-key==2.3.0
+    # via -r requirements/production.in
+drf-yasg==1.21.5
+    # via -r requirements/production.in
+et-xmlfile==1.0.1
+    # via openpyxl
+flake8==6.0.0
+    # via -r requirements/production.in
+gunicorn==20.1.0
+    # via -r requirements/production.in
+idna==2.6
+    # via requests
+inflection==0.3.1
+    # via drf-yasg
+itypes==1.1.0
+    # via coreapi
+jedi==0.17.2
+    # via python-language-server
+jinja2==2.11.1
+    # via coreschema
+kombu==5.2.4
+    # via celery
+markuppy==1.14
+    # via tablib
+markupsafe==1.1.1
+    # via jinja2
+mccabe==0.7.0
+    # via flake8
+odfpy==1.3.6
+    # via tablib
+openpyxl==3.1.2
+    # via tablib
+packaging==23.0
+    # via
+    #   -r requirements/production.in
+    #   build
+    #   drf-yasg
+parso==0.7.1
+    # via jedi
+pillow==9.4.0
+    # via -r requirements/production.in
+pip-tools==6.12.3
+    # via -r requirements/production.in
+pluggy==0.13.1
+    # via python-language-server
+prompt-toolkit==3.0.38
+    # via click-repl
+pycodestyle==2.10.0
+    # via flake8
+pyflakes==3.0.1
+    # via flake8
+pyproject-hooks==1.0.0
+    # via build
+python-jsonrpc-server==0.4.0
+    # via python-language-server
+python-language-server==0.36.2
+    # via -r requirements/production.in
+pytz==2022.7.1
+    # via
+    #   celery
+    #   djangorestframework
+    #   drf-yasg
+pyyaml==5.3
+    # via tablib
+requests==2.22.0
+    # via coreapi
+ruamel-yaml==0.17.21
+    # via drf-yasg
+ruamel-yaml-clib==0.2.7
+    # via ruamel-yaml
+six==1.11.0
+    # via click-repl
+sqlparse==0.3.0
+    # via django
+tablib[html,ods,xls,xlsx,yaml]==3.3.0
+    # via django-import-export
+tomli==2.0.1
+    # via
+    #   build
+    #   pyproject-hooks
+ujson==5.7.0
+    # via
+    #   python-jsonrpc-server
+    #   python-language-server
+uritemplate==3.0.1
+    # via
+    #   coreapi
+    #   drf-yasg
+urllib3==1.25.8
+    # via requests
+vine==5.0.0
+    # via
+    #   amqp
+    #   celery
+    #   kombu
+wcwidth==0.2.6
+    # via prompt-toolkit
+wheel==0.38.4
+    # via pip-tools
+xlrd==1.1.0
+    # via tablib
+xlwt==1.3.0
+    # via tablib
+psycopg2-binary==2.9.5
+    # via -r requirements/production.in
+redis==4.5.1
+    # via -r requirements/production.in
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
+# setuptools
diff --git a/src/account/auth_pipeline.py b/src/account/auth_pipeline.py
index 34ca0180920054311bbad8835511cac3ae367065..75169911a72eea2d59c01ef0e668739966ac7ead 100644
--- a/src/account/auth_pipeline.py
+++ b/src/account/auth_pipeline.py
@@ -1,5 +1,5 @@
 from django.core import exceptions
-from common import email
+from common.email import registration
 
 from . import models
 
@@ -11,4 +11,4 @@ def create_profile(backend, user, response, *args, **kwargs):
         except exceptions.ObjectDoesNotExist:
             models.Profile.objects.create(user=user)
             if user.email is not None:
-                email.registration(user)
+                registration.delay(user)
diff --git a/src/account/serializers.py b/src/account/serializers.py
index 728074ec705b8670c0eb54f36de968e82bce8230..b0b04f62383c14e1017d20def2f8a1e838c3ae69 100644
--- a/src/account/serializers.py
+++ b/src/account/serializers.py
@@ -1,6 +1,6 @@
 from rest_framework import serializers
 from account import models
-from common import email
+from common.email import admitted, denied
 
 
 class ChoiceSerializer(serializers.ModelSerializer):
@@ -59,9 +59,9 @@ class ProfileSerializer_User(serializers.ModelSerializer):
         new_role = validated_data.get('role', instance.role)
         if instance.role != new_role:
             if new_role == 'Student':
-                email.admitted(instance.user)
+                admitted.delay(instance.user)
             if new_role == 'Denied':
-                email.denied(instance.user)
+                denied.delay(instance.user)
         return super().update(instance, validated_data)
 
     def get_full_name(self, obj):
@@ -118,9 +118,9 @@ class ProfileSerializer_Staff(serializers.ModelSerializer):
         new_role = validated_data.get('role', instance.role)
         if instance.role != new_role:
             if new_role == 'Student':
-                email.admitted(instance.user)
+                admitted.delay(instance.user)
             if new_role == 'Denied':
-                email.denied(instance.user)
+                denied.delay(instance.user)
         return super().update(instance, validated_data)
 
     def get_full_name(self, obj):
diff --git a/src/common/email.py b/src/common/email.py
index 53b3ba10b7b6336b21974afa3d3c27c9d0f8160b..4c89adb81196177eed5a89aa60989c4343e25976 100644
--- a/src/common/email.py
+++ b/src/common/email.py
@@ -1,7 +1,9 @@
 import os
-from django.core.mail import EmailMessage
-import codecs
 import sys
+import codecs
+from django.core.mail import EmailMessage
+from celery import shared_task
+
 
 SENDER_EMAIL = os.getenv('SENDER_MAIL', 'kepzes@kszk.bme.hu')
 HOMEWORK_LINK = os.getenv(
@@ -13,7 +15,7 @@ def get_full_name(user):
 
 
 def read_email(name):
-    with codecs.open('common/emails/' + name, 'r', 'utf-8') as myfile:
+    with codecs.open('worker/emails/' + name, 'r', 'utf-8') as myfile:
         data = myfile.read()
     return data
 
@@ -34,30 +36,31 @@ def send_out_mail(subject, message, SENDER_EMAIL, receiver_email):
         email.send()
 
 
+@shared_task()
 def registration(user):
     subject = "KSZKépzés regisztráció"
     message = read_email('registration.txt')
     message = str.format(message % {'name': get_full_name(user)})
     send_out_mail(subject, message, SENDER_EMAIL, [user.email, ])
-    pass
 
 
+@shared_task()
 def admitted(user):
     subject = "Jelentkezés eredménye"
     message = read_email('admitted.txt')
     message = str.format(message % {'name': get_full_name(user)})
     send_out_mail(subject, message, SENDER_EMAIL, [user.email, ])
-    pass
 
 
+@shared_task()
 def denied(user):
     subject = "Jelentkezés eredménye"
     message = read_email('denied.txt')
     message = str.format(message % {'name': get_full_name(user)})
     send_out_mail(subject, message, SENDER_EMAIL, [user.email, ])
-    pass
 
 
+@shared_task()
 def new_homework(user, deadline):
     deadline = deadline.strftime('%Y-%m-%d %H:%M')
     subject = "Új házifeladat"
@@ -65,9 +68,9 @@ def new_homework(user, deadline):
     message = str.format(
         message % {'name': get_full_name(user), 'link': HOMEWORK_LINK, 'deadline': deadline})
     send_out_mail(subject, message, SENDER_EMAIL, [user.email, ])
-    pass
 
 
+@shared_task()
 def homework_corrected(user, title, accepted):
     subject = "Házifeladat eredménye"
     if accepted:
@@ -80,4 +83,3 @@ def homework_corrected(user, title, accepted):
                                     'status': status,
                                     'title': title})
     send_out_mail(subject, message, SENDER_EMAIL, [user.email, ])
-    pass
diff --git a/src/homework/serializers.py b/src/homework/serializers.py
index d2ccfb16c195fadf9eacd35507eb78c1e78b13c7..498e4e01ea0eb0ad0d1aed1ba737a7dcbd40daac 100755
--- a/src/homework/serializers.py
+++ b/src/homework/serializers.py
@@ -1,7 +1,7 @@
 from rest_framework import serializers
 from django.utils import timezone
 from . import models
-from common import email
+from common.email import homework_corrected, new_homework
 
 
 class TaskSerializer(serializers.ModelSerializer):
@@ -17,12 +17,11 @@ class TaskSerializer(serializers.ModelSerializer):
         return data
 
     def create(self, validated_data):
-        # TODO: Worker timeout. Do something with outlook request limitations
-        # profiles = Profile.objects.filter(
-        #     role="Student").exclude(
-        #     user__email='')
-        # for profile in profiles:
-        #     email.new_homework(profile.user, validated_data.get('deadline'))
+        profiles = Profile.objects.filter(
+            role="Student").exclude(
+            user__email='')
+        for profile in profiles:
+            new_homework.delay(profile.user, validated_data.get('deadline'))
         return self.Meta.model.objects.create(**validated_data)
 
 
@@ -61,7 +60,7 @@ class SolutionSerializer_Student(serializers.ModelSerializer):
     def update(self, instance, validated_data):
         if instance.corrected is not True and validated_data.get(
                 'corrected', instance.corrected) is True:
-            email.homework_corrected(
+            homework_corrected.delay(
                 instance.created_by.user,
                 instance.task.title,
                 validated_data.get('accepted', instance.accepted)
@@ -105,7 +104,7 @@ class SolutionSerializer_Staff(serializers.ModelSerializer):
     def update(self, instance, validated_data):
         if instance.corrected is not True and validated_data.get(
                 'corrected', instance.corrected) is True:
-            email.homework_corrected(
+            homework_corrected.delay(
                 instance.created_by.user,
                 instance.task.title,
                 validated_data.get('accepted', instance.accepted)
diff --git a/src/kszkepzes/__init__.py b/src/kszkepzes/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..53f4ccb1d8eb50e131da7b0e3abdd22cf9e0151c 100644
--- a/src/kszkepzes/__init__.py
+++ b/src/kszkepzes/__init__.py
@@ -0,0 +1,3 @@
+from .celery import app as celery_app
+
+__all__ = ("celery_app",)
diff --git a/src/kszkepzes/celery.py b/src/kszkepzes/celery.py
new file mode 100644
index 0000000000000000000000000000000000000000..99d24f1e18df42f038b65343f997cf61acc91d46
--- /dev/null
+++ b/src/kszkepzes/celery.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python3
+
+import os
+from celery import Celery
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kszkepzes.settings.local")
+app = Celery("kszkepzes_worker")
+app.config_from_object("django.conf:settings", namespace="CELERY")
+app.autodiscover_tasks()
diff --git a/src/kszkepzes/settings/local.py b/src/kszkepzes/settings/local.py
index 1c4e0b5cb5ea38e79c195fc5e345f921fb5a8849..b33d4eee82f8ec168872988188abef05a897adcf 100644
--- a/src/kszkepzes/settings/local.py
+++ b/src/kszkepzes/settings/local.py
@@ -9,3 +9,5 @@ EMAIL_USE_TLS = True
 EMAIL_PORT = 587
 EMAIL_HOST_USER = os.getenv('EMAIL')
 EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD')
+CELERY_BROKER_URL = "redis://localhost:6379"
+CELERY_RESULT_BACKEND = "redis://localhost:6379"
diff --git a/src/kszkepzes/settings/production.py b/src/kszkepzes/settings/production.py
index 708db649d24e59cffc227e63f009d092e1ed7706..8ece79a47c289298c9f6abf4caa2e6520eb339de 100644
--- a/src/kszkepzes/settings/production.py
+++ b/src/kszkepzes/settings/production.py
@@ -30,3 +30,7 @@ EMAIL_PORT = 587
 EMAIL_USE_TLS = True
 EMAIL_HOST_USER = os.getenv('SMTP_USER')
 EMAIL_HOST_PASSWORD = os.getenv('SMTP_PASSWORD')
+
+CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL')
+CELERY_RESULT_BACKEND = os.getenv('CELERY_BROKER_URL')
+