diff --git a/testing/py3-falcon/APKBUILD b/testing/py3-falcon/APKBUILD
new file mode 100644
index 0000000000000000000000000000000000000000..507e4ce160b5f09acec4189bb1b52d0c83599994
--- /dev/null
+++ b/testing/py3-falcon/APKBUILD
@@ -0,0 +1,65 @@
+# Maintainer: Noel Kuntze <noel.kuntze@contauro.com>
+pkgname=py3-falcon
+pkgdesc="Web API framework for fast and reliable microservices, proxies, and app backends"
+pkgver=3.1.3
+pkgrel=0
+arch="all"
+url="https://falconframework.org/"
+license="Apache-2.0"
+makedepends="
+	py3-gpep517
+	py3-setuptools
+	py3-wheel
+	python3-dev
+	cython
+	"
+checkdepends="
+	py3-coverage
+	py3-pytest
+	py3-pyaml
+	py3-requests
+	py3-pytest-asyncio
+	py3-aiofiles
+	py3-httpx
+	uvicorn
+	py3-websockets
+	py3-cbor2
+	py3-msgpack
+	py3-mujson
+	py3-ujson
+	py3-rapidjson
+	py3-orjson
+	py3-gunicorn
+	py3-daphne
+	py3-waitress
+	"
+
+subpackages="$pkgname-pyc"
+
+source="$pkgname-$pkgver.tar.gz::https://github.com/falconry/falcon/archive/refs/tags/$pkgver.tar.gz
+	python-3.12.patch
+	"
+
+builddir="$srcdir"/falcon-$pkgver
+
+build() {
+	gpep517 build-wheel \
+		--wheel-dir dist \
+		--output-fd 3 3>&1 >&2
+}
+
+check() {
+	local _site_packages=$(python -c "import site; print(site.getsitepackages()[0])")
+	python3 -m installer -d test_dir dist/*.whl
+	export PYTHONPATH="$PWD/test_dir/$_site_packages:$PYTHONPATH"
+	pytest -vv tests/
+}
+
+package() {
+	python3 -m installer -d "$pkgdir" dist/*.whl
+}
+
+sha512sums="
+66c5f563b373eb2bc2576d64d2225fa98f4d9d80dc1c93f6831f17287738797ac1f08a1ba71f4a70e6bafad7a51f70fa6fbbee99ef521419ceedcf2d892108ee  py3-falcon-3.1.3.tar.gz
+c9b7eb3058df517e7880584292b9a46415f437c9cddff8ddab8d1bcb7cf0e2cc5ef8ebd6e4c5ad2dd243aefc02e4664a91a885cc15b7c97e75ba430bfa669e95  python-3.12.patch
+"
diff --git a/testing/py3-falcon/python-3.12.patch b/testing/py3-falcon/python-3.12.patch
new file mode 100644
index 0000000000000000000000000000000000000000..6bc9b6a2dedcf25c33d9ea26963df897aa8b6112
--- /dev/null
+++ b/testing/py3-falcon/python-3.12.patch
@@ -0,0 +1,116 @@
+From a78cfb38a0c0f6031cc3ff39ff8bf4afd03ef008 Mon Sep 17 00:00:00 2001
+From: Vytautas Liuolia <vytautas.liuolia@gmail.com>
+Date: Thu, 21 Mar 2024 20:59:26 +0100
+Subject: [PATCH] chore(sync): use `asyncio.Runner` for `async_to_sync()` on
+ py311+ (#2216)
+
+* chore(asyncio): replace `get_event_loop()` -> `get_running_loop()` where applicable
+
+* chore(sync): use `asyncio.Runner` for `async_to_sync()` on py311+
+
+* chore(sync): exempt a line from coverage as it can only be hit on 3.11+
+
+* chore(tests/asgi): adapt to Uvicorn now propagating signals to retcode
+
+* chore(tests/asgi): do not check ASGI server retcode on Windows
+
+* chore(tests/asgi): check for a M$ Windows specific exit code constant
+
+* chore(sync): use a nicer pattern to get the active runner
+---
+ falcon/util/sync.py             | 60 ++++++++++++++++++++++++---------
+ pyproject.toml                  |  1 -
+ tests/asgi/test_asgi_servers.py | 10 ++++--
+ tests/asgi/test_scope.py        |  6 ++--
+ tests/dump_asgi.py              |  2 +-
+ 5 files changed, 56 insertions(+), 23 deletions(-)
+
+diff --git a/pyproject.toml b/pyproject.toml
+index ad445ce55..5ed0c5fab 100644
+--- a/pyproject.toml
++++ b/pyproject.toml
+@@ -97,7 +97,6 @@ filterwarnings = [
+     "ignore:.cgi. is deprecated and slated for removal:DeprecationWarning",
+     "ignore:path is deprecated\\. Use files\\(\\) instead:DeprecationWarning",
+     "ignore:This process \\(.+\\) is multi-threaded",
+-    "ignore:There is no current event loop",
+ ]
+ testpaths = [
+     "tests"
+diff --git a/tests/asgi/test_asgi_servers.py b/tests/asgi/test_asgi_servers.py
+index 26f51ad0c..321e41f96 100644
+--- a/tests/asgi/test_asgi_servers.py
++++ b/tests/asgi/test_asgi_servers.py
+@@ -4,6 +4,7 @@
+ import os
+ import platform
+ import random
++import signal
+ import subprocess
+ import sys
+ import time
+@@ -27,7 +28,9 @@
+ _SERVER_HOST = '127.0.0.1'
+ _SIZE_1_KB = 1024
+ _SIZE_1_MB = _SIZE_1_KB**2
+-
++# NOTE(vytas): Windows specific: {Application Exit by CTRL+C}.
++#   The application terminated as a result of a CTRL+C.
++_STATUS_CONTROL_C_EXIT = 0xC000013A
+ 
+ _REQUEST_TIMEOUT = 10
+ 
+@@ -620,7 +623,10 @@ def server_base_url(request):
+ 
+             yield base_url
+ 
+-        assert server.returncode == 0
++        # NOTE(vytas): Starting with 0.29.0, Uvicorn will propagate signal
++        #   values into the return code (which is a good practice in Unix);
++        #   see also https://github.com/encode/uvicorn/pull/1600
++        assert server.returncode in (0, -signal.SIGTERM, _STATUS_CONTROL_C_EXIT)
+ 
+         break
+ 
+diff --git a/tests/asgi/test_scope.py b/tests/asgi/test_scope.py
+index bb60ed0e7..e368f6576 100644
+--- a/tests/asgi/test_scope.py
++++ b/tests/asgi/test_scope.py
+@@ -70,7 +70,7 @@ def test_supported_asgi_version(version, supported):
+     resp_event_collector = testing.ASGIResponseEventCollector()
+ 
+     async def task():
+-        coro = asyncio.get_event_loop().create_task(
++        coro = asyncio.get_running_loop().create_task(
+             app(scope, req_event_emitter, resp_event_collector)
+         )
+ 
+@@ -142,7 +142,7 @@ def test_lifespan_scope_default_version():
+     scope = {'type': 'lifespan'}
+ 
+     async def t():
+-        t = asyncio.get_event_loop().create_task(
++        t = asyncio.get_running_loop().create_task(
+             app(scope, req_event_emitter, resp_event_collector)
+         )
+ 
+@@ -196,7 +196,7 @@ def test_lifespan_scope_version(spec_version, supported):
+         return
+ 
+     async def t():
+-        t = asyncio.get_event_loop().create_task(
++        t = asyncio.get_running_loop().create_task(
+             app(scope, req_event_emitter, resp_event_collector)
+         )
+ 
+diff --git a/tests/dump_asgi.py b/tests/dump_asgi.py
+index 0742a3ca0..0dfdb4b0a 100644
+--- a/tests/dump_asgi.py
++++ b/tests/dump_asgi.py
+@@ -23,5 +23,5 @@ async def app(scope, receive, send):
+         }
+     )
+ 
+-    loop = asyncio.get_event_loop()
++    loop = asyncio.get_running_loop()
+     loop.create_task(_say_hi())