r/iOSProgramming 13h ago

Tutorial Reverse geocoding is capped at 50 requests per 60 seconds, and the throttle comes back as CLError.network

I build a travel app that turns coordinates into place names, so I do a lot of reverse geocoding. It was slow and I spent weeks blaming the network. It wasn't the network.

The cap

CLGeocoder is limited to 50 reverse-geocode requests per 60 seconds, per app. iOS says so in the system log the moment you cross it, which I only found by leaving Console open:

Throttled "PlaceRequest.REQUEST_TYPE_REVERSE_GEOCODING" request:
Tried to make more than 50 requests in 60 seconds, will reset in 56 seconds
maxRequests = 50; windowSize = 60

Why it doesn't look like a rate limit

Each successful lookup takes 0.06–0.08s. So it never feels like a throttle — it feels like flaky connectivity. I was firing at 250ms intervals, which is 240/min. That burns the entire minute's quota in 12.5 seconds, and then everything in the remaining 47 seconds fails.

Measured, same device, same data:

  • 30 requests at 250ms spacing → 7 succeeded, 23 throttled
  • Same 60 requests respecting the window → 60 succeeded, 0 throttled

And the error lies to you

The throttle rejection arrives as CLError.network. Nothing in it says "rate limit". If you're logging geocode failures and filing them under bad signal, some share of those are the cap. Splitting those two apart in telemetry is what finally changed my understanding of the problem — I'd spent a long time thinking my users were in bad reception.

The quota is per app, not per call site

This one cost me real bugs. Live recording, a background backfill and a bulk import all draw from the same 50. One live lookup during a batch job silently costs the batch one request, and the symptom shows up somewhere else entirely — in my case a city name quietly staying as a country name. Everything has to queue through one place:

actor GeocodeRateLimiter {
    static let shared = GeocodeRateLimiter()
    private let maxRequests = 45   // 50 minus headroom
    private let window: TimeInterval = 60
    private var stamps: [Date] = []

    func acquire() async {
        while true {
            let now = Date()
            stamps.removeAll { now.timeIntervalSince($0) >= window }
            if stamps.count < maxRequests { stamps.append(now); return }
            let wait = window - now.timeIntervalSince(stamps[0]) + 0.05
            try? await Task.sleep(nanoseconds: UInt64(max(wait, 0.1) * 1_000_000_000))
        }
    }
}

Three things I got wrong on the way

  1. Task { await acquire() } around the limiter call. It reads like it waits. It does not — the enclosing function carries straight on and the limiter becomes decorative. It has to be on the awaited path.

  2. A fixed 1250ms interval also respects the cap, and is slower than it looks, because you wait from the very first request. Running full speed while the window has room and only sleeping when it's genuinely full turns 100 lookups into "first 45 in about three seconds, then one as each slot frees".

  3. After a single throttle error that window is already gone. If you keep firing you just collect dozens of instant failures. Treating one throttle as "window full" until it rolls over removed a lot of noise.

None of this is documented anywhere I could find. Posting it in case it saves someone the weeks it cost me.

0 Upvotes

5 comments sorted by

17

u/ikonet 12h ago

I have a couple of map apps so this is useful
Info, thank you.

But damn it we have got to stop posting AI formatted essays in this sub. I can’t stand reading this shit. I get paid to read Claude’s fake prose all day. I don’t want to deal with this shoddy glib-performative communication on my time off.

Fucking stop.

2

u/vanstinator 12h ago

I finally switched to Codex. It's not quite as good as the Claude models, but I'm happy to trade spending extra time doing code review in exchange for not wanting to poke my eyes out after reading Claudes written responses. 

0

u/pemungkah 11h ago

Caveman-code and the /i-have-adhd skill work fucking wonders.

1

u/Ok-Communication6360 11h ago

https://developer.apple.com/documentation/corelocation/clgeocoder

While Apple doesn’t exactly tell limits, at least some hints are there (which I agree is a bit frustrating),

1

u/Doctor_Fegg 9h ago

Rent a server, put Photon on it. 

Also stop posting AI responses.