So you want to: Rate Limit access to your API

tomchiverton edited this page Aug 6, 2014 · 2 revisions
Clone this wiki locally

This example is implemented in /examples/api_rateLimited (included in the download).

Like most other complex problems, there are a lot of ways to solve this one. What is presented here is a simple form of rate limiting that you can add to your application. If you need "enterprise-scale" rate limiting, you'll want to look into hardware solutions or dedicated middleware.

Application.cfc:

<cfcomponent extends="taffy.core.api">
    <cfscript>
        this.name = "rate_limiting_example";

        function onApplicationStart(){
            super.onApplicationStart();
            application.accessLog = queryNew('apiKey,accessTime','varchar,time');
            application.accessLimit = 100; //requests
            application.accessPeriod = 60; //seconds
        }

        function onTaffyRequest(verb, cfc, requestArguments, mimeExt){
            var usage = 0;

            //require some api key
            if (!structKeyExists(requestArguments, "apiKey")){
                return newRepresentation().noData().withStatus(401, "API Key Required");
            }

            //check usage
            usage = getAccessRate(requestArguments.apiKey);
            if (usage lte application.accessLimit){
                logAccess(requestArguments.apiKey);
                return true;
            }else{
                return newRepresentation().noData().withStatus(420, "Enhance your calm");
            }

            return true;
        }
    </cfscript>

    <cffunction name="getAccessRate" access="private" output="false">
        <cfargument name="apiKey" required="true" />
        <cfset var local = structNew() />
        <!--- now get matches for the current api key --->
        <cfquery name="local.accessLookup" dbtype="query">
            select accessTime
            from application.accessLog
            where apiKey = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.apiKey#" />
            and accessTime > <cfqueryparam cfsqltype="cf_sql_timestamp" value="#dateAdd("s",(-1 * application.accessPeriod),now())#" />
        </cfquery>
        <!--- if access log is getting long, do some cleanup --->
        <cfif local.accessLookup.recordCount gt application.accessLimit>
            <cfset pruneAccessLog() />
        </cfif>
        <cfreturn local.accessLookup.recordCount />
    </cffunction>

    <cffunction name="logAccess" access="private" output="true">
        <cfargument name="apiKey" required="true" type="string" />
        <cfset var qLog = '' />
        <cflock timeout="10" type="readonly" name="logging">
            <cfset queryAddRow(application.accessLog)/>
            <cfset querySetCell(application.accessLog, "accessTime", now()) />
            <cfset querySetCell(application.accessLog, "apiKey", arguments.apiKey) />
        </cflock>
    </cffunction>

    <cffunction name="pruneAccessLog" access="private" output="false">
        <cflock timeout="10" type="readonly" name="logging">
            <cfquery name="application.accessLog" dbtype="query">
                delete
                from application.accessLog
                where accessTime < <cfqueryparam cfsqltype="cf_sql_timestamp" value="#dateAdd("s",(-1 * application.accessPeriod),now())#" />
            </cfquery>
        </cflock>
    </cffunction>

</cfcomponent>

This code sets a limit of 100 requests per 60 seconds. It uses a rolling-total method, so if 10 requests are made in the first second of access, followed by 90 over the next few seconds, then exactly 60 seconds from the first access another 10 requests will be allowed, and more requests will become available after more seconds pass.