diff --git a/src/videoag/authentication/ViewPermissions.tsx b/src/videoag/authentication/ViewPermissions.tsx
index e572693864ab9a639847598ad0bc92e312c7ed9f..dca98f621506c00b55056a500e179ee612414b95 100644
--- a/src/videoag/authentication/ViewPermissions.tsx
+++ b/src/videoag/authentication/ViewPermissions.tsx
@@ -9,7 +9,7 @@ import { showError, showErrorToast } from "@/videoag/error/ErrorDisplay";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
 import { useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
 import { StylizedText } from "@/videoag/miscellaneous/StylizedText";
-import { useDebounceUpdateData } from "@/videoag/miscellaneous/PromiseHelpers";
+import { useDebouncedCall } from "@/videoag/miscellaneous/PromiseHelpers";
 
 export function AuthenticationMethodIcons({
     authentication_methods,
@@ -163,8 +163,6 @@ function PasswordAuthComponent({ course, lecture }: { course: course; lecture: l
     );
 }
 
-class DummyObject {}
-
 function OAuthComponent({
     type,
     course,
@@ -193,54 +191,51 @@ function OAuthComponent({
         };
     }, []);
 
-    // useDebounceUpdateData is not quite intended for this, but works perfectly in ensuring we only do one fetch at a time
-    const [fetchStatusDeferred, fetchStatusNow] = useDebounceUpdateData<DummyObject, void>(
-        (dummy) =>
-            api
-                .getAuthenticationStatus({ lecture_id: lecture.id })
-                .then((res) => {
-                    if (res.is_lecture_authenticated) {
-                        setIsOAuthRunning(false);
-                        setWaitingForFinish(false);
-                        authStatus.addAuthenticatedMethod(type);
-                        authStatus.setCurrentlyAuthenticatedLectureId(lecture.id);
-                    } else if (res.in_progress_authentication !== type) {
+    const [fetchStatusDeferred, fetchStatusNow] = useDebouncedCall<void>(1000, () =>
+        api
+            .getAuthenticationStatus({ lecture_id: lecture.id })
+            .then((res) => {
+                if (res.is_lecture_authenticated) {
+                    setIsOAuthRunning(false);
+                    setWaitingForFinish(false);
+                    authStatus.addAuthenticatedMethod(type);
+                    authStatus.setCurrentlyAuthenticatedLectureId(lecture.id);
+                } else if (res.in_progress_authentication !== type) {
+                    setIsOAuthRunning(false);
+                    setWaitingForFinish(false);
+                    setOauthInfo(
+                        <span className="text-warning">
+                            {language.get("ui.video_player.login_oauth.finished_no_access")}
+                        </span>,
+                    );
+                } else {
+                    if (remainingFinishAttempts.current > 0) {
+                        remainingFinishAttempts.current--;
+                        fetchStatusDeferred();
+                    } else if (popupClosed.current) {
                         setIsOAuthRunning(false);
                         setWaitingForFinish(false);
                         setOauthInfo(
                             <span className="text-warning">
-                                {language.get("ui.video_player.login_oauth.finished_no_access")}
+                                {language.get(
+                                    "ui.video_player.login_oauth.unfinished_popup_closed",
+                                )}
                             </span>,
                         );
-                    } else {
-                        if (remainingFinishAttempts.current > 0) {
-                            remainingFinishAttempts.current--;
-                            fetchStatusDeferred(new DummyObject());
-                        } else if (popupClosed.current) {
-                            setIsOAuthRunning(false);
-                            setWaitingForFinish(false);
-                            setOauthInfo(
-                                <span className="text-warning">
-                                    {language.get(
-                                        "ui.video_player.login_oauth.unfinished_popup_closed",
-                                    )}
-                                </span>,
-                            );
-                        } else if (remainingFinishAttempts.current <= 0) {
-                            console.log("Stopping automatic authentication status fetching");
-                        }
+                    } else if (remainingFinishAttempts.current <= 0) {
+                        console.log("Stopping automatic authentication status fetching");
                     }
-                })
-                .catch((e) => {
-                    showError(e, language.get("ui.video_player.login_oauth.error_finish"));
-                    remainingFinishAttempts.current = 0;
-                    if (popupClosed.current) {
-                        setIsOAuthRunning(false);
-                        setWaitingForFinish(false);
-                        setOauthInfo(undefined);
-                    }
-                }),
-        1000,
+                }
+            })
+            .catch((e) => {
+                showError(e, language.get("ui.video_player.login_oauth.error_finish"));
+                remainingFinishAttempts.current = 0;
+                if (popupClosed.current) {
+                    setIsOAuthRunning(false);
+                    setWaitingForFinish(false);
+                    setOauthInfo(undefined);
+                }
+            }),
     );
 
     const startFinishListener = (popup: any) => {
@@ -248,13 +243,13 @@ function OAuthComponent({
         setOauthInfo(undefined);
         popupClosed.current = false;
         remainingFinishAttempts.current = 120;
-        fetchStatusDeferred(new DummyObject());
+        fetchStatusDeferred();
 
         let popInterval = setInterval(() => {
             if (popup.closed) {
                 remainingFinishAttempts.current = 1;
                 popupClosed.current = true;
-                fetchStatusNow(new DummyObject());
+                fetchStatusNow();
                 clearInterval(popInterval);
             }
         }, 100);
diff --git a/src/videoag/job/Job.tsx b/src/videoag/job/Job.tsx
index 880651c11ded7bf5d4c72a99a7b9df9944b8c869..fb9f08ae102cd440fea815db9201a8ace0017634 100644
--- a/src/videoag/job/Job.tsx
+++ b/src/videoag/job/Job.tsx
@@ -1,9 +1,10 @@
 import { useState, useEffect, useRef } from "react";
 
-import type { int, job } from "@/videoag/api/types";
+import type { int, job, job_state } from "@/videoag/api/types";
 import { useApi } from "@/videoag/api/ApiProvider";
 import { datetimeToString } from "@/videoag/miscellaneous/Formatting";
 import { Spinner } from "@/videoag/miscellaneous/Util";
+import { useDebouncedCall } from "@/videoag/miscellaneous/PromiseHelpers";
 
 export function JobStatusCard({ jobId }: { jobId: int }) {
     const api = useApi();
@@ -13,10 +14,16 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
     const [errorMsg, setErrorMsg] = useState<string | undefined>(undefined);
     const doAutomaticReload = useRef<boolean>(false);
 
-    const reloadStatus = () => {
-        if (!doAutomaticReload.current) return;
+    const canJobStatusChange = (state: job_state) =>
+        ["ready", "spawning", "running"].includes(state);
+
+    const [reloadStatusDeferred, reloadStatusNow] = useDebouncedCall(2000, () => {
+        if (!doAutomaticReload.current) {
+            return Promise.resolve();
+        }
         setIsLoading(true);
-        api.getJobStatus({ job_ids: [jobId] })
+        return api
+            .getJobStatus({ job_ids: [jobId] })
             .then((res) => {
                 const updateData = res.jobs[jobId.toString()];
                 setJob((old) => ({
@@ -24,11 +31,8 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
                     ...updateData,
                 }));
                 setErrorMsg(undefined);
-                if (
-                    doAutomaticReload.current &&
-                    ["ready", "spawning", "running"].includes(updateData.state)
-                )
-                    setTimeout(reloadStatus, 2000);
+                if (doAutomaticReload.current && canJobStatusChange(updateData.state))
+                    reloadStatusDeferred();
             })
             .catch((e) => {
                 setErrorMsg("Unable to update status: " + e.toString());
@@ -36,7 +40,7 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
             .finally(() => {
                 setIsLoading(false);
             });
-    };
+    });
 
     useEffect(() => {
         setIsLoading(true);
@@ -44,10 +48,13 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
         setErrorMsg(undefined);
         api.getJob(jobId)
             .then((res) => {
-                setJob(res.job_context[jobId.toString()]);
+                const newJob = res.job_context[jobId.toString()];
+                setJob(newJob);
                 setErrorMsg(undefined);
                 doAutomaticReload.current = true;
-                setTimeout(reloadStatus, 2000);
+                if (canJobStatusChange(newJob.state)) {
+                    reloadStatusDeferred();
+                }
             })
             .catch((e) => {
                 setErrorMsg("Unable to load job: " + e.toString());
diff --git a/src/videoag/miscellaneous/PromiseHelpers.tsx b/src/videoag/miscellaneous/PromiseHelpers.tsx
index 5c568b5f5d41ce63eceab54b1a4ef457239d58a0..8d62cddcf7858febd2ae6e2f50d23174a90cc621 100644
--- a/src/videoag/miscellaneous/PromiseHelpers.tsx
+++ b/src/videoag/miscellaneous/PromiseHelpers.tsx
@@ -103,37 +103,70 @@ export function useDebounceWithArgument<V, R>(
     ];
 }
 
-export function useDebounceUpdateData<D, R>(
-    updater: (newData: D) => Promise<R>,
+// Returns a function which calls the provided function while ensuring only one call is running at a time
+export function useMutexCall<R>(call: () => Promise<R>): () => Promise<R> {
+    const [deferred, now] = useDebouncedProcessing(0, (data) => call(), true);
+    return () => now(undefined);
+}
+
+// A function which processes data while ensuring only one processor is running at a time
+export function useMutexProcessing<D, R>(
+    processor: (newData: D) => Promise<R>,
+    reprocessEqualInput: boolean = false,
+): (newData: D) => Promise<R> {
+    const [deferred, now] = useDebouncedProcessing(0, processor, reprocessEqualInput);
+    return now;
+}
+
+// A function which calls the given function with an optional delay while ensuring only one call is running at a time
+export function useDebouncedCall<R>(
     delay: number,
+    call: () => Promise<R>,
+): [() => void, () => Promise<R>] {
+    const [deferred, now] = useDebouncedProcessing(delay, (data) => call(), true);
+    return [() => deferred(undefined), () => now(undefined)];
+}
+
+class NoDataMarker {}
+
+// A function which processes data with an optional delay while ensuring only one processor is running at a time
+// Usually intended for API calls to update/fetch data
+export function useDebouncedProcessing<D, R>(
+    delay: number,
+    processor: (newData: D) => Promise<R>,
+    reprocessEqualInput: boolean = false,
 ): [(newData: D) => void, (newData: D) => Promise<R>] {
     const timeoutIdRef = useRef<NodeJS.Timeout>(undefined);
     const processingPromiseRef = useRef<Promise<R> | undefined>(undefined);
-    const unprocessedDataRef = useRef<D | undefined>(undefined);
+    const unprocessedDataRef = useRef<D | NoDataMarker>(new NoDataMarker());
 
-    function updateData(): Promise<R> | undefined {
+    function doProcessing(): Promise<R> | undefined {
         if (processingPromiseRef.current !== undefined) {
             return processingPromiseRef.current;
         }
-        if (unprocessedDataRef.current === undefined) {
+        if (unprocessedDataRef.current instanceof NoDataMarker) {
             return undefined;
         }
         const data = unprocessedDataRef.current;
-        const onUpdateFinished = (arg: any) => {
+        const onProcessingFinished = (arg: any) => {
             processingPromiseRef.current = undefined;
-            if (unprocessedDataRef.current !== undefined) {
-                if (unprocessedDataRef.current === data) {
-                    unprocessedDataRef.current = undefined;
+            if (!(unprocessedDataRef.current instanceof NoDataMarker)) {
+                if (!reprocessEqualInput && unprocessedDataRef.current === data) {
+                    unprocessedDataRef.current = new NoDataMarker();
                 } else if (timeoutIdRef.current === undefined) {
                     // Only run immediately when no timeout is set. A call to deferred should still be deferred even if
-                    // an update was running during the call.
-                    updateData();
+                    // a processor was running during the call.
+                    doProcessing();
                 }
             }
             return arg;
         };
-        unprocessedDataRef.current = undefined;
-        const promise = updater(data).then(onUpdateFinished, onUpdateFinished);
+        unprocessedDataRef.current = new NoDataMarker();
+        const processorPromise = processor(data);
+        if (!processorPromise) {
+            throw new Error(`Processor '${processor}' did not return a promise`);
+        }
+        const promise = processorPromise.then(onProcessingFinished, onProcessingFinished);
         processingPromiseRef.current = promise;
         return promise;
     }
@@ -145,7 +178,7 @@ export function useDebounceUpdateData<D, R>(
 
             const timeoutRef = setTimeout(() => {
                 if (timeoutIdRef.current === timeoutRef) timeoutIdRef.current = undefined;
-                updateData();
+                doProcessing();
             }, delay);
             timeoutIdRef.current = timeoutRef;
         },
@@ -153,7 +186,7 @@ export function useDebounceUpdateData<D, R>(
             clearTimeout(timeoutIdRef.current);
             timeoutIdRef.current = undefined;
             unprocessedDataRef.current = newData;
-            return updateData()!;
+            return doProcessing()!;
         },
     ];
 }
diff --git a/src/videoag/object_management/OMConfigComponent.tsx b/src/videoag/object_management/OMConfigComponent.tsx
index f242926acb15f414d8235a1575ad9cefc7328b8d..02ff6b7b359829ecf09f45f422ff8df7fedb0254 100644
--- a/src/videoag/object_management/OMConfigComponent.tsx
+++ b/src/videoag/object_management/OMConfigComponent.tsx
@@ -14,7 +14,7 @@ import { ApiError } from "@/videoag/api/ApiError";
 import { showError, showErrorToast } from "@/videoag/error/ErrorDisplay";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
 import { useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
-import { useDebounceUpdateData } from "@/videoag/miscellaneous/PromiseHelpers";
+import { useDebouncedProcessing } from "@/videoag/miscellaneous/PromiseHelpers";
 import { datetimeToString, semesterToHuman } from "@/videoag/miscellaneous/Formatting";
 import { StylizedText } from "@/videoag/miscellaneous/StylizedText";
 import { deepEquals, TooltipButton } from "@/videoag/miscellaneous/Util";
@@ -196,7 +196,8 @@ export function EmbeddedOMFieldComponent({
             });
     }, [api, isEditing, object_type, object_id, field_id]);
 
-    const [deferredSendValueUpdate, nowSendValueUpdate] = useDebounceUpdateData<any, void>(
+    const [deferredSendValueUpdate, nowSendValueUpdate] = useDebouncedProcessing<any, void>(
+        isInstantEdit ? 10 : 1000 * 15,
         async (newValue: any) => {
             if (field === undefined) return;
             if (deepEquals(newValue, serverSideFieldValueRef.current)) return;
@@ -219,7 +220,6 @@ export function EmbeddedOMFieldComponent({
                     showError(error, "Unable to update field");
                 });
         },
-        isInstantEdit ? 10 : 1000 * 15,
     );
 
     const disableEditing = () => {