-
Notifications
You must be signed in to change notification settings - Fork 117
So you want to: Rate Limit access to your API
tomchiverton edited this page Aug 6, 2014
·
2 revisions
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.