Как уже заметили коллеги - первое что нужно выяснить - что тормозит. 
Спойлер: 90% что проблема в запросах к бд, так как сам по себе код в большинстве случаев банально отсылает запросы и принимает их результат, выводя его пользователю в несложных действиях. В редких случаях - запросы идут к внешним тормозящим сервисам, например к фейсбуку или другим СС. И крайне редко тормозит сам код, но тут уже опять же - 90% что это некомпетентность разраба, например, не способного построить нормальный запрос и сортирующий данные от бд в коде...
Что конкретно надо делать:
1) Взять за шкирку самого разбирающегося в коде (если таковой существует в проекте), и дать задание описать базовый стек вызовов, если это какой-то самопис, или сразу расставить код замера времени с метками в случае понятной архитектуры. Что то типа такого(класс - древний костыль, но работает, так что матом не ругаться):
Class Timer.php:
class Timer {
    static $start;
    static $end;
    static $marks = [];
    static $formats = [1=>''];
    static function init(){
        if(empty(self::$start)) self::$start = microtime(true);
    }
    static function setMark($markName = ''){
        $time = microtime(true);
        if($markName == '')$markName = $time;
        $data['name'] = $markName;
        $data['time'] = $time;
        $res['time'] = $time;
        if(count(self::$marks) > 1)$res['diff'] = $time - self::$marks[count(self::$marks)-2]['time'];
        else $res['diff'] = 0;
        $data['diff'] = $res['diff'];
        self::$marks[] = $data;
        return $res;
    }
    static function timeFormat($number,$format = ''){
        if(empty($format)) $format = 3;
        return number_format ($number,$format,'.','');
    }
    static function report(){
        self::$end = microtime(true);
        self::$marks['start'] = self::$start;
        self::$marks['end'] = self::$end;
        self::$marks['all_time'] =  self::$end - self::$start;
        if(!empty(self::$marks)) return self::$marks;
    }
}
In code:
\Timer::init()
//some code block 1
\Timer::setMark('after block 1');
//some code block 2
\Timer::setMark('after block 2');
...
//some code block n
\Timer::setMark('after block n');
//near end of code 
\Timer::setMark('end');
var_dump(\Timer::report());
exit;
2) Смотрите на блоки жрущие время, делите их до атомарных операций путем деления блоков пополам таймерами. 
3) Смотрите что там происходит - оптимизируете*. И так по кругу. 
4) Профит.
* Оптимизация
Запросы: 
1) Смотреть план запроса (use explain, Luke!).
2) Расставить индексы которых явно не хватает
3) Смотреть не вызывается ли 50 запросов в цикле? Если да - выписать пенделя писавшему, затем переписать в 1 запрос с нормальным джоином.
Код: 
Иногда запрос сложно оптимизировать, он вытаскивает много данных, хотя эти данные не часто обновляются. Такие запросы нужно кешировать, для чего используют быстрые ин-мемори хранилища типа редис или мемкеш. В крайнем случае в файлах...
Чаще всего код тормозит на регулярках, хотя "хороший" программист может придумать и более креативные способы погреть процессор.
Что нужно сделать обязательно кроме тестов и как тогда лучше спрашивать с разработчиков, если они предлагают размытые предложения? Хочется понять в какую сторону копать
Бить палкой не вариант? Тогда берите других, эти испортились. Если разработчик не знает как выявить узкие места кода - нахрена  он нужен? Код написать сегодня любой чат может... Ну, на крайняк дайте им вышеприведенный вариант решения проблемы...
PS: Кстати, сервер может банально не выдерживать наплыв сетевых соединений, пните адимна, пусть глянет логи.
PPS: 
достаточно 2000-4000 человек, заходящих в течение 20 минут на сайт
это равномерные 3-4 рпс, ну или пусть в пике 50 рпс, должно держать даже на несложной конфигурации... Копайте код.