Skip to content

Rate Limits

FHIR Directory applies rate limits to protect the infrastructure and keep the service responsive for all callers. The exact limits and how they apply depend on your plan and whether you're using an API key — always check the RateLimit-* response headers to see your current quota rather than hard-coding assumptions.

Response headers

Every response includes rate limit headers:

RateLimit-Limit: <your current limit>
RateLimit-Remaining: <remaining requests in window>
RateLimit-Reset: <unix timestamp when window resets>
HeaderDescription
RateLimit-LimitMaximum requests allowed in the current window
RateLimit-RemainingRequests remaining before the limit is reached
RateLimit-ResetUnix timestamp (seconds) when the window resets

Read RateLimit-Remaining on every response and back off gracefully as it approaches zero — don't hard-code a specific limit into your client.

When you're rate limited

If you exceed the limit, the API returns 429 Too Many Requests:

json
{
  "success": false,
  "error": {
    "code": "TOO_MANY_REQUESTS",
    "message": "Rate limit exceeded. Authenticate with an API key for higher limits."
  }
}

The response includes a Retry-After header with the number of seconds to wait:

Retry-After: 45

Best practices

  1. Cache responses — endpoint data changes at most once per day. Cache results for at least an hour.
  2. Use pagination wisely — fetch 100 results per page instead of 25 to reduce total requests.
  3. Respect Retry-After — wait the specified time before retrying, don't hammer the API.
  4. Batch your work — if you need all endpoints for a vendor, paginate through once and store locally.
  5. Debounce search inputs — if you're building a search UI, debounce user input to avoid firing a request on every keystroke.

Debounce example for search UIs

If you're using FHIR Directory as a search-as-you-type backend, debounce the input to avoid hitting the rate limit. Here are examples for popular frameworks:

tsx
import { useState, useEffect } from "react";

function EndpointSearch() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!query.trim()) { setResults([]); return; }

    const timer = setTimeout(async () => {
      const response = await fetch(
        `https://fhir-api.luxera.io/api/v1/endpoints?query=${encodeURIComponent(query)}`
      );
      const { data } = await response.json();
      setResults(data);
    }, 400);

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search by name"
      />
      {results.map((ep) => (
        <div key={ep.id}>{ep.organizationName} — {ep.vendorName}</div>
      ))}
    </>
  );
}
vue
<script setup lang="ts">
import { ref, watch } from "vue";

const query = ref("");
const results = ref([]);
let timer: ReturnType<typeof setTimeout>;

watch(query, (value) => {
  clearTimeout(timer);
  if (!value.trim()) { results.value = []; return; }

  timer = setTimeout(async () => {
    const response = await fetch(
      `https://fhir-api.luxera.io/api/v1/endpoints?query=${encodeURIComponent(value)}`
    );
    const { data } = await response.json();
    results.value = data;
  }, 400);
});
</script>

<template>
  <input v-model="query" placeholder="Search by name" />
  <div v-for="ep in results" :key="ep.id">
    {{ ep.organizationName }} — {{ ep.vendorName }}
  </div>
</template>
svelte
<script>
  let query = "";
  let results = [];
  let timer;

  function handleInput() {
    clearTimeout(timer);
    if (!query.trim()) { results = []; return; }

    timer = setTimeout(async () => {
      const response = await fetch(
        `https://fhir-api.luxera.io/api/v1/endpoints?query=${encodeURIComponent(query)}`
      );
      const { data } = await response.json();
      results = data;
    }, 400);
  }
</script>

<input bind:value={query} on:input={handleInput} placeholder="Search by name" />
{#each results as ep}
  <div>{ep.organizationName} — {ep.vendorName}</div>
{/each}
typescript
function debounce<T extends (...args: any[]) => void>(fn: T, ms: number) {
  let timer: ReturnType<typeof setTimeout>;
  return (...args: Parameters<T>) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

async function searchEndpoints(query: string) {
  const response = await fetch(
    `https://fhir-api.luxera.io/api/v1/endpoints?query=${encodeURIComponent(query)}`
  );
  const { data } = await response.json();
  renderResults(data);
}

const debouncedSearch = debounce(searchEndpoints, 400);

document.querySelector("#search-input")
  .addEventListener("input", (e) => {
    debouncedSearch((e.target as HTMLInputElement).value);
  });

Each example fires the API call only after the user stops typing for 400ms, reducing a fast typist's 10+ requests into 1 and keeping you well within your quota.

Example: handle rate limits gracefully

python
import requests
import time

def get_with_retry(url, params=None, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(url, params=params)
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            print(f"Rate limited. Waiting {retry_after}s...")
            time.sleep(retry_after)
            continue
        return response
    raise Exception("Max retries exceeded")

response = get_with_retry(
    "https://fhir-api.luxera.io/api/v1/endpoints",
    params={"vendor": "epic", "limit": 100}
)
typescript
async function getWithRetry(url: string, maxRetries = 3): Promise<Response> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url);
    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get("Retry-After") ?? "60");
      console.log(`Rate limited. Waiting ${retryAfter}s...`);
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      continue;
    }
    return response;
  }
  throw new Error("Max retries exceeded");
}

const response = await getWithRetry(
  "https://fhir-api.luxera.io/api/v1/endpoints?vendor=epic&limit=100"
);
csharp
async Task<HttpResponseMessage> GetWithRetry(string url, int maxRetries = 3)
{
    using var client = new HttpClient();
    for (var attempt = 0; attempt < maxRetries; attempt++)
    {
        var response = await client.GetAsync(url);
        if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
        {
            var retryAfter = response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 60;
            Console.WriteLine($"Rate limited. Waiting {retryAfter}s...");
            await Task.Delay(TimeSpan.FromSeconds(retryAfter));
            continue;
        }
        return response;
    }
    throw new Exception("Max retries exceeded");
}

var response = await GetWithRetry(
    "https://fhir-api.luxera.io/api/v1/endpoints?vendor=epic&limit=100"
);

Need higher limits?

For production applications that need higher throughput, contact us.

Built by Luxera Software