diff --git a/plugins/login-resources/package.json b/plugins/login-resources/package.json
index 67af413c25..873418b304 100644
--- a/plugins/login-resources/package.json
+++ b/plugins/login-resources/package.json
@@ -6,10 +6,12 @@
   "license": "EPL-2.0",
   "scripts": {
     "build": "compile ui",
+    "test": "jest --passWithNoTests --silent",
     "build:docs": "api-extractor run --local",
     "format": "format src",
     "build:watch": "compile ui",
     "_phase:build": "compile ui",
+    "_phase:test": "jest --passWithNoTests --silent",
     "_phase:format": "format src",
     "_phase:validate": "compile validate"
   },
diff --git a/plugins/login-resources/src/__tests__/mutex.test.ts b/plugins/login-resources/src/__tests__/mutex.test.ts
new file mode 100644
index 0000000000..58afbd34a6
--- /dev/null
+++ b/plugins/login-resources/src/__tests__/mutex.test.ts
@@ -0,0 +1,28 @@
+import { makeSequential } from '../mutex'
+
+describe('mutex', () => {
+  let results: number[]
+
+  beforeEach(() => {
+    results = []
+  })
+
+  async function waitAndPushResult (millis: number): Promise<void> {
+    await new Promise<void>((resolve) => {
+      setTimeout(resolve, millis)
+    })
+    results.push(millis)
+  }
+
+  it('expect sequential execution', async () => {
+    const myUpdate = makeSequential(waitAndPushResult)
+    await Promise.all([myUpdate(50), myUpdate(30), myUpdate(40), myUpdate(10), myUpdate(20)])
+    expect(results).toEqual([50, 30, 40, 10, 20])
+  })
+
+  it('expect parallel execution', async () => {
+    const myUpdate = waitAndPushResult
+    await Promise.all([myUpdate(50), myUpdate(30), myUpdate(40), myUpdate(10), myUpdate(20)])
+    expect(results).toEqual([10, 20, 30, 40, 50])
+  })
+})
diff --git a/plugins/login-resources/src/components/Form.svelte b/plugins/login-resources/src/components/Form.svelte
index 7f86b7a642..8c1f053036 100644
--- a/plugins/login-resources/src/components/Form.svelte
+++ b/plugins/login-resources/src/components/Form.svelte
@@ -31,6 +31,7 @@
   import { onMount } from 'svelte'
   import { BottomAction, getHref } from '..'
   import login from '../plugin'
+  import { makeSequential } from '../mutex'
   import Providers from './Providers.svelte'
 
   interface Field {
@@ -66,7 +67,7 @@
 
   $: $themeStore.language && validate($themeStore.language)
 
-  async function validate (language: string): Promise<boolean> {
+  const validate = makeSequential(async function validateAsync (language: string): Promise<boolean> {
     if (ignoreInitialValidation) return true
     for (const field of fields) {
       const v = object[field.name]
@@ -101,7 +102,7 @@
     }
     status = OK
     return true
-  }
+  })
   validate($themeStore.language)
 
   let inAction = false
diff --git a/plugins/login-resources/src/mutex.ts b/plugins/login-resources/src/mutex.ts
new file mode 100644
index 0000000000..b45530db9a
--- /dev/null
+++ b/plugins/login-resources/src/mutex.ts
@@ -0,0 +1,84 @@
+//
+// Copyright © 2022 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+/**
+  Class that allows synchronization of async functions to prevent race conditions.
+  Inspired by https://stackoverflow.com/a/51086893
+**/
+class Mutex {
+  currentLock: Promise<void>
+
+  constructor () {
+    // initially the lock is not held
+    this.currentLock = Promise.resolve()
+  }
+
+  /**
+    Acquires the lock. Usage example:
+
+    ```
+    let mutex = new Mutex()
+
+    async function synchronizedFunction() {
+      const unlockMutex = await mutex.lock()
+      try {
+        // critical section
+      } finally {
+        unlockMutex()
+      }
+    }
+    ```
+
+    @return {Promise<VoidFunction>} promise that must be awaited in order to acquire the lock.
+    When the promise is fulfulled it returns a function that must be invoked to release the lock.
+  **/
+  async lock (): Promise<VoidFunction> {
+    // function which invocation releases the lock
+    let releaseLock: VoidFunction
+    // this Promise is fulfilled as soon as the function above is invoked
+    const afterReleasePromise = new Promise<void>((resolve) => {
+      releaseLock = () => {
+        resolve()
+      }
+    })
+    // Caller gets a promise that resolves
+    // only after the current outstanding lock resolves
+    const blockingPromise = this.currentLock.then(() => releaseLock)
+    // Don't allow the next request until the new promise is done
+    this.currentLock = afterReleasePromise
+    // Return the new promise
+    return await blockingPromise
+  }
+}
+
+type AnyFunction<R> = (...args: any[]) => Promise<R>
+/**
+  This function wraps around the Mutex implementation above and provides a simple interface.
+
+  @return {Promise<VoidFunction>} a sequential version of the passed function. This version guarantees
+  that its invocations are executed one by one.
+**/
+export function makeSequential<R, T extends AnyFunction<R>> (fn: T): (...args: Parameters<T>) => Promise<R> {
+  const mutex = new Mutex()
+
+  return async function (...args: Parameters<T>): Promise<R> {
+    const unlockMutex = await mutex.lock()
+    try {
+      return await fn(...args)
+    } finally {
+      unlockMutex()
+    }
+  }
+}