Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dd62442
Added aiofastnet to speedups extra
Jun 6, 2026
eaa4be2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 6, 2026
2f9457e
Remove unused imports, rename change file
Jun 6, 2026
62e234f
Fix flake8 complains
Jun 6, 2026
655f953
aiofastnet supports TLS over TLS always
Jun 6, 2026
0a2a8d8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 6, 2026
4bb0887
Fix flake errors
Jun 6, 2026
226ee8f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 6, 2026
0069c22
Fix lint and mypy error
Jun 6, 2026
fb772b6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 6, 2026
32ee277
Update FAQ
Jun 6, 2026
38dba91
Revert changes in test_connector_multiple_event_loop
Jun 6, 2026
b5d3521
Add 'Conda' to spelling list
Jun 6, 2026
e4c899e
Revert name change
Jun 6, 2026
3cdc4f9
Merge branch 'master' into feature/speedups_aiofastnet
tarasko Jun 8, 2026
b19d66a
Merge branch 'master' into feature/speedups_aiofastnet
Dreamsorcerer Jun 8, 2026
63fd43f
Apply suggestions from code review
Dreamsorcerer Jun 8, 2026
fcbc84f
Apply suggestions from code review
Dreamsorcerer Jun 9, 2026
e558301
Update aiofastnet to 0.10.0
Jun 10, 2026
fe149b9
Upgrade to aiofastnet 0.11.0, fix sendfile return type
Jun 11, 2026
780bdd1
Fix unreachable mypy error
Jun 11, 2026
db52793
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2026
0a92e74
Add type info for sendfile arguments
Jun 11, 2026
dc21a31
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2026
4960d64
Add type info for create_connection wrapper
Jun 11, 2026
f60851a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2026
86e6d78
Add type info for create_server
Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES/12822.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added ``aiofastnet`` package to ``speedups`` extra. aiofastnet provides faster alternatives to the standard loop functions, which are used to run server or establish connections. If you experience any issues that you think might be related to this change, you can try to disable ``aiofastnet`` by uninstalling aiofastnet package.

-- by :user:`tarasko`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ Sunit Deshpande
Sviatoslav Bulbakha
Sviatoslav Sydorenko
Taha Jahangir
Taras Kozlov
Taras Voinarovskyi
Terence Honles
Thanos Lefteris
Expand Down
102 changes: 95 additions & 7 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
from .log import client_logger
from .resolver import DefaultResolver

try:
import aiofastnet
except ImportError:
aiofastnet = None # type: ignore[assignment]


if sys.version_info >= (3, 12):
from collections.abc import Buffer
else:
Expand Down Expand Up @@ -98,6 +104,82 @@
from .tracing import Trace


async def create_connection(
loop: asyncio.AbstractEventLoop,
protocol_factory: Callable[[], ResponseHandler],
*,
ssl: SSLContext | None,
sock: socket.socket,
server_hostname: str | None,
ssl_shutdown_timeout: float | None = None,
) -> tuple[asyncio.Transport, ResponseHandler]:
if aiofastnet is not None:
return await aiofastnet.create_connection(
loop,
protocol_factory,
ssl=ssl,
sock=sock,
server_hostname=server_hostname,
ssl_shutdown_timeout=ssl_shutdown_timeout,
)
else:
if sys.version_info >= (3, 11): # type: ignore[unreachable]
return await loop.create_connection(
protocol_factory,
ssl=ssl,
sock=sock,
server_hostname=server_hostname,
ssl_shutdown_timeout=ssl_shutdown_timeout,
)
else:
return await loop.create_connection(
protocol_factory,
ssl=ssl,
sock=sock,
server_hostname=server_hostname,
)


async def start_tls(
loop: asyncio.AbstractEventLoop,
transport: asyncio.Transport,
protocol: ResponseHandler,
sslcontext: SSLContext,
*,
server_hostname: str | None,
ssl_handshake_timeout: float | None,
ssl_shutdown_timeout: float | None = None,
) -> asyncio.BaseTransport | None:
if aiofastnet is not None:
return await aiofastnet.start_tls(
loop,
transport,
protocol,
sslcontext,
server_hostname=server_hostname,
ssl_handshake_timeout=ssl_handshake_timeout,
ssl_shutdown_timeout=ssl_shutdown_timeout,
)
else:
if sys.version_info >= (3, 11): # type: ignore[unreachable]
return await loop.start_tls(
transport,
protocol,
sslcontext,
server_hostname=server_hostname,
ssl_handshake_timeout=ssl_handshake_timeout,
ssl_shutdown_timeout=ssl_shutdown_timeout,
)
else:
return await loop.start_tls(
transport,
protocol,
sslcontext,
server_hostname=server_hostname,
ssl_handshake_timeout=ssl_handshake_timeout,
)


class Connection:
"""Represents a single connection."""

Expand Down Expand Up @@ -1266,7 +1348,7 @@ async def _wrap_create_connection(
and sys.version_info >= (3, 11)
):
kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout
return await self._loop.create_connection(*args, **kwargs, sock=sock)
return await create_connection(self._loop, *args, **kwargs, sock=sock)
except cert_errors as exc:
raise ClientConnectorCertificateError(req.connection_key, exc) from exc
except ssl_errors as exc:
Expand Down Expand Up @@ -1297,10 +1379,14 @@ def _warn_about_tls_in_tls(
return

# Support in asyncio was added in Python 3.11 (bpo-44011)
asyncio_supports_tls_in_tls = sys.version_info >= (3, 11) or getattr(
underlying_transport,
"_start_tls_compatible",
False,
asyncio_supports_tls_in_tls = (
sys.version_info >= (3, 11)
or getattr(
underlying_transport,
"_start_tls_compatible",
False,
)
or aiofastnet is not None
)

if asyncio_supports_tls_in_tls:
Expand Down Expand Up @@ -1347,7 +1433,8 @@ async def _start_tls_connection(
try:
# ssl_shutdown_timeout is only available in Python 3.11+
if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout:
tls_transport = await self._loop.start_tls(
tls_transport = await start_tls(
self._loop,
underlying_transport,
tls_proto,
sslcontext,
Expand All @@ -1356,7 +1443,8 @@ async def _start_tls_connection(
ssl_shutdown_timeout=self._ssl_shutdown_timeout,
)
else:
tls_transport = await self._loop.start_tls(
tls_transport = await start_tls(
self._loop,
underlying_transport,
tls_proto,
sslcontext,
Expand Down
30 changes: 24 additions & 6 deletions aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from mimetypes import MimeTypes
from stat import S_ISREG
from types import MappingProxyType
from typing import IO, TYPE_CHECKING, Any, Final, Optional
from typing import TYPE_CHECKING, BinaryIO, Final, Optional

from . import hdrs
from .abc import AbstractStreamWriter
Expand All @@ -30,10 +30,28 @@
if TYPE_CHECKING:
from .web_request import BaseRequest

try:
import aiofastnet
except ImportError:
aiofastnet = None # type: ignore[assignment]


_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]]


async def sendfile(
loop: asyncio.AbstractEventLoop,
transport: asyncio.Transport,
file: BinaryIO,
offset: int,
count: int,
) -> None:
if aiofastnet is not None:
await aiofastnet.sendfile(loop, transport, file, offset, count)
else:
await loop.sendfile(transport, file, offset, count) # type: ignore[unreachable]


NOSENDFILE: Final[bool] = bool(os.environ.get("AIOHTTP_NOSENDFILE"))

CONTENT_TYPES: Final[MimeTypes] = MimeTypes()
Expand Down Expand Up @@ -92,12 +110,12 @@ def __init__(
self._path = pathlib.Path(path)
self._chunk_size = chunk_size

def _seek_and_read(self, fobj: IO[Any], offset: int, chunk_size: int) -> bytes:
def _seek_and_read(self, fobj: BinaryIO, offset: int, chunk_size: int) -> bytes:
fobj.seek(offset)
return fobj.read(chunk_size) # type: ignore[no-any-return]
return fobj.read(chunk_size)

async def _sendfile_fallback(
self, writer: AbstractStreamWriter, fobj: IO[Any], offset: int, count: int
self, writer: AbstractStreamWriter, fobj: BinaryIO, offset: int, count: int
) -> AbstractStreamWriter:
# To keep memory usage low,fobj is transferred in chunks
# controlled by the constructor's chunk_size argument.
Expand All @@ -118,7 +136,7 @@ async def _sendfile_fallback(
return writer

async def _sendfile(
self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int
self, request: "BaseRequest", fobj: BinaryIO, offset: int, count: int
) -> AbstractStreamWriter:
writer = await super().prepare(request)
assert writer is not None
Expand All @@ -132,7 +150,7 @@ async def _sendfile(
raise ConnectionResetError("Connection lost")

try:
await loop.sendfile(transport, fobj, offset, count)
await sendfile(loop, transport, fobj, offset, count)
except NotImplementedError:
return await self._sendfile_fallback(writer, fobj, offset, count)

Expand Down
51 changes: 48 additions & 3 deletions aiohttp/web_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import signal
import socket
from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import Any, Generic, TypeVar

from yarl import URL
Expand All @@ -21,6 +22,49 @@
except ImportError: # pragma: no cover
SSLContext = object # type: ignore[misc,assignment]

try:
import aiofastnet
except ImportError:
aiofastnet = None # type: ignore[assignment]


async def create_server(
loop: asyncio.AbstractEventLoop,
protocol_factory: Callable[[], asyncio.Protocol],
host: str | None = None,
port: int | None = None,
*,
sock: socket.socket | None = None,
ssl: SSLContext | None = None,
backlog: int = 100,
reuse_address: bool | None = None,
reuse_port: bool | None = None,
) -> asyncio.Server:
if aiofastnet is not None:
return await aiofastnet.create_server(
loop,
protocol_factory,
host,
port,
sock=sock,
ssl=ssl,
backlog=backlog,
reuse_address=reuse_address,
reuse_port=reuse_port,
)
else:
return await loop.create_server( # type: ignore[unreachable]
protocol_factory,
host,
port,
sock=sock,
ssl=ssl,
backlog=backlog,
reuse_address=reuse_address,
reuse_port=reuse_port,
)


__all__ = (
"BaseSite",
"TCPSite",
Expand Down Expand Up @@ -130,7 +174,8 @@ async def start(self) -> None:
loop = asyncio.get_running_loop()
server = self._runner.server
assert server is not None
self._server = await loop.create_server(
self._server = await create_server(
loop,
server,
self._host,
self._port,
Expand Down Expand Up @@ -244,8 +289,8 @@ async def start(self) -> None:
loop = asyncio.get_running_loop()
server = self._runner.server
assert server is not None
self._server = await loop.create_server(
server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog
self._server = await create_server(
loop, server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog
)


Expand Down
61 changes: 61 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,67 @@ enable compression in NGINX (you are deploying aiohttp behind reverse
proxy, right?).


How do I enable Kernel TLS, and should I do it?
-----------------------------------------------

Kernel TLS (KTLS) allows aiohttp to move encryption and decryption of
TLS traffic from user space to the kernel. It was added to the Linux kernel in
4.13, but full support for TLS 1.3 and modern ciphers is available only
since 5.19.

KTLS will be beneficial if you run an HTTPS server that often returns
:class:`~aiohttp.web.FileResponse` objects or you have a high-end NIC that can
offload TLS encryption. For ordinary
dynamic responses, small files, or deployments behind a TLS-terminating reverse
proxy, it is unlikely to help and may actually slightly degrade performance.

KTLS is supported through the ``aiofastnet`` package, which is installed as
part of the ``speedups`` extra.

To enable KTLS, you have to do and check the following:

* Verify that ``aiofastnet`` is installed and can be imported.

Currently, ``aiofastnet`` works only with CPython distributions that are
dynamically linked against OpenSSL. This is generally true for system Python
installations, Conda distributions, ``pyenv``, and
``actions/setup-python`` in GitHub Actions, but not for Python installations
managed by ``uv``.

.. code-block:: python

try:
import aiofastnet
except ImportError:
aiofastnet = None

* Make sure the Linux ``tls`` kernel module is loaded::

sudo modprobe tls

* Make sure the ``ssl.OP_ENABLE_KTLS`` option is enabled in ``SSLContext``
(available since Python 3.12)::

sslcontext.options |= ssl.OP_ENABLE_KTLS

* Make sure Python is using OpenSSL 3.0 or newer. OpenSSL should have been
built on a machine whose Linux headers are new enough. OpenSSL needs Linux
headers at least 4.13.0 to build the transmit path; older headers make it
skip KTLS support. Typically, Python is using the system OpenSSL on Linux,
but some times distributions ship their own OpenSSL. The following commands
will help identify the OpenSSL version and which ``libssl`` and ``libcrypto``
are being used by the ``ssl`` module::

python -c "import ssl; print(ssl.OPENSSL_VERSION)"
ldd "$(python -c 'import _ssl; print(_ssl.__file__)')"


If ``ssl.OP_ENABLE_KTLS`` was requested in ``sslcontext``, but ``aiofastnet``
could not enable KTLS, it will log a warning suggesting the possible reason.

After enabling it, run your own benchmarks and verify that KTLS actually
speeds things up in your case.

How do I manage a ClientSession within a web server?
----------------------------------------------------

Expand Down
Loading
Loading