swiftapinsurlsessionurlsessionnsurlsessiondownloadtask

Limit API Calls to 40 per minute (Swift)


I have a limit of 40 URL Session calls to my API per minute.

I have timed the number of calls in any 60s and when 40 calls have been reached I introduced sleep(x). Where x is 60 - seconds remaining before new minute start. This works fine and the calls don’t go over 40 in any given minute. However the limit is still exceeded as there might be more calls towards the end of the minute and more at the beginning of the next 60s count. Resulting in an API error.

I could add a:

usleep(x)

Where x would be 60/40 in milliseconds. However as some large data returns take much longer than simple queries that are instant. This would increase the overall download time significantly.

Is there a way to track the actual rate to see by how much to slow the function down?


Solution

  • Might not be the neatest approach, but it works perfectly. Simply storing the time of each call and comparing it to see if new calls can be made and if not, the delay required.

    Using previously suggested approach of delay before each API call of 60/40 = 1.5s (Minute / CallsPerMinute), as each call takes a different time to produce response, total time taken to make 500 calls was 15min 22s. Using the below approach time taken: 11min 52s as no unnecessary delay has been introduced.

    Call before each API Request:

    API.calls.addCall()
    

    Call in function before executing new API task:

    let limit = API.calls.isOverLimit()
                    
    if limit.isOver {
        sleep(limit.waitTime)
    }
    

    Background Support Code:

    var globalApiCalls: [Date] = []
    
    public class API {
    
    let limitePerMinute = 40 // Set API limit per minute
    let margin = 2 // Margin in case you issue more than one request at a time
    
    static let calls = API()
    
    func addCall() {
        globalApiCalls.append(Date())
    }
    
    func isOverLimit() -> (isOver: Bool, waitTime: UInt32)
    {
        let callInLast60s = globalApiCalls.filter({ $0 > date60sAgo() })
        
        if callInLast60s.count > limitePerMinute - margin {
            if let firstCallInSequence = callInLast60s.sorted(by: { $0 > $1 }).dropLast(2).last {
                let seconds = Date().timeIntervalSince1970 - firstCallInSequence.timeIntervalSince1970
                if seconds < 60 { return (true, UInt32(60 + margin) - UInt32(seconds.rounded(.up))) }
            }
        }
        return (false, 0)
    }
    
    private func date60sAgo() -> Date
    {
        var dayComponent = DateComponents(); dayComponent.second = -60
        return Calendar.current.date(byAdding: dayComponent, to: Date())!
    }
    }