How to live with external API limits on the number of requests

Many services provide the ability to interact with them not only for ordinary users through polished and optimized graphical interfaces, but also for external developers from their programs through the API. At the same time, it is important for services to control the load on their infrastructure. In the situation with ordinary users, most of the load problems will not arise due to the control of the application code sending requests to the service by the service developers (users trying to do something in the application outside the framework of the interfaces proposed by the developers and the documented capabilities, we are in this article not considered). In the case of external developers, the scope for creating a load on the service is limited only by the imagination of these very external developers. To limit this space a little,the practice of introducing restrictions on the number of requests per unit of time to the service API has become widespread. 

, , API , , «» API.

, Data Platfrom ManyChat. , , , Intercom, In-App -. , Intercom (, , ..). Intercom - -, . , , ( ), , -. , ML- . , Intercom.

: , , API 1000 . , Intercom, .

, API , . «» «» -, .

- , « » API, API, API. , , API .

« »

, ManyChat Redis — . « » - , API . , API, «», , - . , , «» , - Intercom, , «» .

Redis, List .

, API, consumer API. rate-limit, , , .

— «» - ( BackendQueue), «» (AnalyticsQueue). , , consumer, , .

(JSON):

{
    "method_name": "users_update", //  ,   
    "parameters": {"user_id": 123} // ,       
}

MVP consumer'a (PHP)
class APICaller
{
    private const RETRIES_LIMIT = 5;
    private const RATE_LIMIT_TIMEFRAME = 10;
    
    ...
    
    public function callMethod(array $payload): void
    {
        switch ($payload['method_name']) {
            case 'users_update':
                $this->getIntercomAPI()->users->update($payload['parameters']);
                break;
            default:
                throw new \RuntimeException('Unknown method in API call');
        }
    }

    public function actionProcessQueue(): void
    {
        while (true) {
            $payload = $this->getRedis()->rawCommand('LPOP', 'BackendQueue');
            if ($payload === null) {
                $payload = $this->getRedis()->rawCommand('LPOP', 'AnalyticsQueue');
            }

            if ($payload) {
                $retries = 0;
                $processed = false;
                while ($processed === false && $retries < self::RETRIES_LIMIT)
                {
                    try {
                        $this->callMethod(json_decode($payload));
                        $processed = true;
                    } catch (IntercomRateLimitException $e) {
                        $retries++;
                        sleep(self::RATE_LIMIT_TIMEFRAME);
                    }
                }
            } else {
                sleep(1);
            }
        }
    }
}

, , — .

:

Backend (PHP):

...
$payload = [
    'method_name' => 'users_update',
    'parameters' => ['user_id' => 123, 'registration_date' => '2020-10-01'],
];
$this->getRedis()->rawCommand('RPUSH', 'BackendQueue', json_encode($payload));
...

(Python):

...
payload = {
    'method_name': 'users_update',
    'parameters': {'user_id': 123, 'advanced_metric': 42},
}
redis_client.rpush('AnalyticsQueue', json.dumps(payload))
...

— , Intercom, . — - , API «» , rate-limit, customer'a rate-limit', , - . Redis ( ) consumer'. , , consumer', , . , , , , .

, , consumer' , . consumer' , API .

consumer'a (PHP)
class APICaller
{
    private const RETRIES_LIMIT = 5;
    private const RATE_LIMIT_TIMEFRAME = 10;
    private const INTERCOM_RATE_LIMIT = 150;
    private const INTERCOM_API_WORKERS = 5;

    ...

    public function callMethod(array $payload): void
    {
        switch ($payload['method_name']) {
            case 'users_update':
                $this->getIntercomAPI()->users->update($payload['parameters']);
                break;
            default:
                throw new \RuntimeException('Unknown method in API call');
        }
    }

    public function actionProcessQueue(): void
    {
        $currentTimeframe = $this->getCurrentTimeframe();
        $currentRequestCount = 0;
        
        while (true) {
            if ($currentTimeframe !== $this->getCurrentTimeframe()) {
                $currentTimeframe = $this->getCurrentTimeframe();
                $currentRequestCount = 0;
            } elseif ($currentRequestCount > $this->getProcessRateLimit()) {
                usleep(100 * 1000);
                continue;
            }
            
            $payload = $this->getRedis()->rawCommand('LPOP', 'BackendQueue');
            if ($payload === null) {
                $payload = $this->getRedis()->rawCommand('LPOP', 'AnalyticsQueue');
            }

            if ($payload) {
                $retries = 0;
                $processed = false;
                while ($processed === false && $retries < self::RETRIES_LIMIT)
                {
                    try {
                        $this->callMethod(json_decode($payload));
                        $processed = true;
                    } catch (IntercomRateLimitException $e) {
                        $retries++;
                        sleep(self::RATE_LIMIT_TIMEFRAME);
                    }
                }
            } else {
                sleep(1);
            }
        }
    }

    private function getProcessRateLimit(): int
    {
        return (int) floor(self::INTERCOM_RATE_LIMIT / self::INTERCOM_API_WORKERS);
    }

    private function getCurrentTimeframe(): int
    {
        return (int) ceil(time() / self::RATE_LIMIT_TIMEFRAME);
    }
}

API

- API, . API . — . , , callback'e, consumer' . callback', , .

, , , , .

, , //?

, API , rate-limit, . , . , , , , , .

, ,   .

API, , , API , API .

, - . , API .




All Articles