Rate limit on FieldPulse API and handling 429 errors

I have been reviewing the API Rate Limits and Best Practices documentation, but I would like to clarify several points regarding the exact semantics of the throttling mechanism.

Specifically:

  1. Is the rate limit enforced per API key, per account, or per IP address? I.e., if multiple services share a single account's API credentials, do they share a single quota?
  2. What are the exact header values returned on a 429 response? I have observed X-RateLimit-Remaining and X-RateLimit-Reset in other systems, but I want to confirm the FieldPulse implementation before implementing client-side logic.
  3. Is the reset window a fixed calendar window (e.g., top of minute) or a rolling window from first request? This materially affects backoff calculations for bursty traffic patterns.
  4. Are there different tiers or limits for read vs. write operations? I.e., does GET /work_orders consume quota identically to POST /work_orders?

I am also interested in any edge cases or failure modes: e.g., if a request is rate-limited, are idempotent retries safe for non-idempotent methods (POST, PATCH) given potential server-side partial processing?

Finally, has anyone benchmarked the actual vs. documented limits? I prefer empirical data over specification when designing resilient systems.

Parents
  • Great questions, Omar — this is exactly the kind of detail that matters when you're building something that needs to stay up.

    Quick answers:

    • Limit scope: Per API key, not per account or IP. Two keys on the same account = separate buckets.
    • Headers: We return X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix timestamp). The reset is rolling from your first request in the window, not fixed.
    • Read vs. write: Same quota consumption currently, though we're evaluating weighted limits. No promises on timeline.

    For the idempotency question: 429s are returned before your request hits the application layer, so you're safe to retry. If you get a 429, nothing was written. That said, for POST/PATCH I'd still recommend using idempotency keys (we support Idempotency-Key headers) because 5xx responses are a different story.

    Here's a minimal retry handler in Python that respects the reset header:

    import time
    import requests
    from functools import wraps
    
    def rate_limit_aware(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            max_retries = 5
            for attempt in range(max_retries):
                resp = func(*args, **kwargs)
                if resp.status_code != 429:
                    return resp
                reset_ts = int(resp.headers.get('X-RateLimit-Reset', time.time() + 60))
                sleep_for = max(reset_ts - time.time(), 1)  # min 1s buffer
                time.sleep(sleep_for * (2 ** attempt))  # exponential backoff
            raise Exception("Rate limited after max retries")
        return wrapper
    
    @rate_limit_aware
    def call_api():
        return requests.get("">api.fieldpulse.io/.../work_orders", headers={...})
    

    Yeah, the rolling window bit tripped me up too when I first looked at it — fixed windows are way easier to reason about, but rolling is what we've got for now.

    Docs link: API Rate Limits and Best Practices — I've noted we should clarify the rolling vs. fixed distinction more explicitly.

Reply
  • Great questions, Omar — this is exactly the kind of detail that matters when you're building something that needs to stay up.

    Quick answers:

    • Limit scope: Per API key, not per account or IP. Two keys on the same account = separate buckets.
    • Headers: We return X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix timestamp). The reset is rolling from your first request in the window, not fixed.
    • Read vs. write: Same quota consumption currently, though we're evaluating weighted limits. No promises on timeline.

    For the idempotency question: 429s are returned before your request hits the application layer, so you're safe to retry. If you get a 429, nothing was written. That said, for POST/PATCH I'd still recommend using idempotency keys (we support Idempotency-Key headers) because 5xx responses are a different story.

    Here's a minimal retry handler in Python that respects the reset header:

    import time
    import requests
    from functools import wraps
    
    def rate_limit_aware(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            max_retries = 5
            for attempt in range(max_retries):
                resp = func(*args, **kwargs)
                if resp.status_code != 429:
                    return resp
                reset_ts = int(resp.headers.get('X-RateLimit-Reset', time.time() + 60))
                sleep_for = max(reset_ts - time.time(), 1)  # min 1s buffer
                time.sleep(sleep_for * (2 ** attempt))  # exponential backoff
            raise Exception("Rate limited after max retries")
        return wrapper
    
    @rate_limit_aware
    def call_api():
        return requests.get("">api.fieldpulse.io/.../work_orders", headers={...})
    

    Yeah, the rolling window bit tripped me up too when I first looked at it — fixed windows are way easier to reason about, but rolling is what we've got for now.

    Docs link: API Rate Limits and Best Practices — I've noted we should clarify the rolling vs. fixed distinction more explicitly.

Children
  • Thank you, Eli. The clarification on application-layer vs. edge-layer rejection is particularly useful — I had not confirmed that 429s are returned pre-processing.

    A follow-up: regarding the X-RateLimit-Reset header, is the timestamp in UTC epoch seconds, or milliseconds? I have encountered both conventions and want to avoid off-by-1000 errors in my backoff calculations.

    Also, has the engineering team published any empirical throughput benchmarks, or is the documented limit (I believe 100 req/min) a hard ceiling observed in practice?