Rate limits
PropSocket rate-limits at 150 requests per minute per Organization. That's the number to plan around — not "reasonable limits," not a soft suggestion. Read thex-ratelimit-* headers on every response, honor Retry-After on a 429, and you'll never get surprised in production.
Per Organization, not per key
The limit is accounted at the Organization level. Every API key your Organization holds draws from the same 150 req/min pool — issuing a second key splits the budget, it doesn't double it. If you run several services against PropSocket, they share one bucket; size your concurrency accordingly. Keys being Organization-scoped is covered in authentication.
Throttle, then hard limit
There are two thresholds inside the per-minute window:
| Usage | Behavior |
|---|---|
| Up to 80% (≤ 120 req/min) | Full speed. No added latency. |
| 80%–100% (120–150 req/min) | Throttle. Responses get a 2-second delay to push back before you hit the wall. Requests still succeed. |
| 100% (> 150 req/min) | Hard limit. Further requests return 429 until the minute window resets. |
The 2-second throttle is a feature, not a failure — it slows you to a sustainable pace before you start collecting 429s. If you see latency climb to ~2s, that's the signal to ease off.
Headers
Every response carries your current standing:
HTTP/1.1 200 OK
x-ratelimit-limit: 150
x-ratelimit-remaining: 112
X-Request-ID: req_01HX9P3K2N7QZRWY4B8MJ5VCDFx-ratelimit-limit— your ceiling (150 by default).x-ratelimit-remaining— requests left in the current window.
Watch x-ratelimit-remaining and slow down as it approaches zero, rather than waiting for the 429. The custom limit shown in x-ratelimit-limit reflects any per-contract override on your Organization.
The 429 response
When you cross 100%, you get a 429 with a Retry-After header in seconds. Sleep for exactly that long — don't guess a backoff, the server already told you. The body is the standard RFC 7807 problem+json shape.
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 150
x-ratelimit-remaining: 0
Retry-After: 23
Content-Type: application/problem+json
{
"type": "https://docs.propsocket.io/errors/rate_limit_exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Retry after 23 seconds.",
"instance": "/v1/units",
"request_id": "req_01HX9P3K2N7QZRWY4B8MJ5VCDF"
}Handle 429 with backoff
A minimal client that honors Retry-After and retries transparently:
import os, time, requests
BASE = "https://api.propsocket.io/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PROPSOCKET_API_KEY']}"}
def get(path, **params):
while True:
resp = requests.get(f"{BASE}/{path}", headers=HEADERS, params=params)
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", "1"))
time.sleep(wait) # honor the server's wait, don't guess
continue
resp.raise_for_status()
return resp.json()
print(get("units", limit=100)["meta"])const BASE = "https://api.propsocket.io/v1";
const HEADERS = { Authorization: `Bearer ${process.env.PROPSOCKET_API_KEY}` };
async function get(path, params = {}) {
const url = new URL(`${BASE}/${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
for (;;) {
const resp = await fetch(url, { headers: HEADERS });
if (resp.status === 429) {
const wait = Number(resp.headers.get("Retry-After") ?? "1");
await new Promise((r) => setTimeout(r, wait * 1000)); // honor the header
continue;
}
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
}
console.log((await get("units", { limit: 100 })).meta);package main
import (
"encoding/json"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
const base = "https://api.propsocket.io/v1"
func get(path string, params url.Values) map[string]any {
key := os.Getenv("PROPSOCKET_API_KEY")
for {
req, _ := http.NewRequest("GET", base+"/"+path+"?"+params.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
if resp.StatusCode == 429 {
wait, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
resp.Body.Close()
time.Sleep(time.Duration(wait) * time.Second) // honor the header
continue
}
if resp.StatusCode != 200 {
resp.Body.Close()
panic("HTTP " + strconv.Itoa(resp.StatusCode))
}
var body map[string]any
json.NewDecoder(resp.Body).Decode(&body)
resp.Body.Close()
return body
}
}require "net/http"
require "json"
require "uri"
BASE = "https://api.propsocket.io/v1"
KEY = ENV.fetch("PROPSOCKET_API_KEY")
def get(path, **params)
loop do
uri = URI("#{BASE}/#{path}")
uri.query = URI.encode_www_form(params)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{KEY}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
if res.code.to_i == 429
sleep(res["Retry-After"].to_i) # honor the server's wait, don't guess
next
end
raise "HTTP #{res.code}" unless res.code.to_i == 200
return JSON.parse(res.body)
end
end
puts get("units", limit: 100)["meta"]<?php
$base = "https://api.propsocket.io/v1";
$key = getenv("PROPSOCKET_API_KEY");
function get(string $base, string $key, string $path, array $params): array
{
while (true) {
$ch = curl_init("$base/$path?" . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($raw, 0, $headerSize);
$payload = substr($raw, $headerSize);
curl_close($ch);
if ($status === 429) {
preg_match("/retry-after:\s*(\d+)/i", $headers, $m);
sleep((int) ($m[1] ?? 1)); // honor the header
continue;
}
if ($status !== 200) {
throw new RuntimeException("HTTP $status");
}
return json_decode($payload, true);
}
}
print_r(get($base, $key, "units", ["limit" => 100])["meta"]);import java.net.URI;
import java.net.http.*;
import com.fasterxml.jackson.databind.*; // Jackson: the one JSON dependency
var base = "https://api.propsocket.io/v1";
var key = System.getenv("PROPSOCKET_API_KEY");
var client = HttpClient.newHttpClient();
var mapper = new ObjectMapper();
JsonNode get(String path, String query) throws Exception {
while (true) {
var request = HttpRequest.newBuilder(URI.create(base + "/" + path + "?" + query))
.header("Authorization", "Bearer " + key).build();
var resp = client.send(request, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 429) {
int wait = resp.headers().firstValue("Retry-After")
.map(Integer::parseInt).orElse(1);
Thread.sleep(wait * 1000L); // honor the header
continue;
}
return mapper.readTree(resp.body());
}
}
System.out.println(get("units", "limit=100").get("meta"));using System.Text.Json;
var BASE = "https://api.propsocket.io/v1";
var key = Environment.GetEnvironmentVariable("PROPSOCKET_API_KEY");
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);
async Task<JsonElement> Get(string path, string query)
{
while (true)
{
var resp = await client.GetAsync($"{BASE}/{path}?{query}");
if ((int)resp.StatusCode == 429)
{
var wait = resp.Headers.RetryAfter?.Delta?.TotalSeconds ?? 1;
await Task.Delay(TimeSpan.FromSeconds(wait)); // honor the header
continue;
}
resp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
return doc.RootElement.Clone();
}
}
Console.WriteLine((await Get("units", "limit=100")).GetProperty("meta"));Backfills and large loads
150 req/min is plenty for steady-state reads, but a cold backfill of a large warehouse needs a plan. The math is simple: at the max page size of 100 records per request (seepagination), 150 requests/minute moves up to 15,000 records per minute — roughly 900,000 per hour if you stay just under the throttle.
- Use the max page size.
limit=100means fewer requests per record. This is the single biggest lever. - Page sequentially per entity, sorted by
created_at:asc. Don't fan out parallel offsets against one entity — that's a correctness bug, not just a throughput one (see pagination). - Parallelize across entities, not within one. One worker per entity (properties, units, residents, leases) is safe and shares the same pool — keep total concurrency low enough to stay under 120 req/min and avoid the throttle.
- Cap your client at ~2 requests/second as a simple ceiling — that's 120/min, below the throttle threshold, leaving headroom for your steady-state traffic.
For a 100k-unit load, that's well under ten minutes of wall-clock time at full page size. Backfill once with created_at:asc, then keep current withwebhooks instead of re-polling — webhooks are on theScale plan and above (pricing).
Next
Decode the 429 body in the error reference, or plan a clean backfill with the pagination guide.