Самый примитивный способ — ограничить глубину запроса (это называется Query depth limiting).
Чуть продвинутее — вычислить сложность запроса на основе размера ответа (это называется Query cost/complexity analysis): каждому полю задается вес, веса полей на одном уровне вложенности складываются, веса вложенных полей перемножаются, задается пороговое значение, если финальный вес запроса меньше порогового — выполняем запрос, если больше — возвращаем ошибку.
Веса можно взять из Apollo Studio. Если подключить отправку телеметрии своего GraphQL API в Apollo Studio, то через какое-то время там накопится статистика в миллисекундах. Если, например, одно поле резолвится в среднем за 13 мс и мы даем ему вес 1, то поле, которое резолвится в среднем за 39 мс, будет иметь вес 3.
Но и это не панацея, злоумышленник может методом подбора вычислить пороговое значение на сервере и сконструировать такой запрос, сложность которого лишь немного меньше порогового и делать такой запрос к серверу часто (и сервер будет обязан выполнять запрос). Поэтому можно ограничить частоту запросов (количество запросов в течение фиксированного временного окна) (это называется Fixed-size time window (rate) limiting).
Причем одно другое другое не исключает, а дополняет. Нужно использовать и Query cost/complexity analysis, и Fixed-size time window (rate) limiting вместе.