Первое что бросается в глаза так это то что сессия создаётся для каждого запроса, в итоге питон очень много времени затрачивает на инициализацию и подготовку подключений, сессиию надо создать в create_gather и после передовать в main.
aiohttp под капотом имеет лимит в 100 tcp подключений в пуле, если на сервере ресурсов хватает то конечно хотелось бы иметь возможность держать хотя-бы 500 подключений на один воркер, ещё aiohttp получая ответ приводит заголовки в CMultiDict, лично у меня на тестах он работал в 20 раз медленее чем стандартный словарь
Я бы заменил aiohttp на httpcore, httpcore это минимальный клиентский интерфейс который используется в другой популярной библиотеке httpx.
httpcore работает с байтами, заголовки с ответа элементарно сплитятся и возвращаются в виде списка кортежей, можно задать хоть 1000 подключений в пуле, настроить keep-alive и слать запросы по протоколу HTTP/2.0, результат в разы быстрее чем aiohttp.
Создав большое количество подключений + задач столько же или в разы больше то скрипт начнёт подвисать на обработке цикла событий, чтобы снизить издержки стоит установить uvloop, он работает под linux.
Вместо bs4 я бы использовал parsel, по скоростям не могу сказать, чисто вкусовщина.
Если учитывать что у вас там милион доменов и они все разные то скорее всего вам нужны какие-то общие данные в виде тегов meta/title/h1 то быстрее будет написать свою функцию для анализа html.
psycopg нужно заменить на asyncpg он очень быстро преобразует python типы в типы postgresql.
asyncpg позволяет создать пул подключений к базе а после создать воркеров в количестве созданных подключений, каждый воркер должен прослушивать asyncio.Queue, в очередь можно сразу закидывать аргументы для asyncpg.
Все данные желательно кэшировать локально в dict, в словаре хоть миллион хоть 100 миллионов ключей, доступ к ним по методу .get() отрабатывается за доли микросекунды но потребление оперативной будет не малой. Пришедший и распарсенный ответ чекаем в локальном кэше, если данные изменились то отправляем задачу в очередь на обновление, если нет такого ключа то аппем куда-то значения для одного запроса INSERT с множеством VALUES, для обработки INSERT отдельную задачу надо сделать чтобы чекать раз в несколько секунд VALUES и если они есть то генерить SQL и отправлять в очередь.