SOS API Rate Limits: Why Live Lookups Throttle Per State and How to Design for Queueing

September 25, 2026
September 24, 2026
16 Minutes Read
Secretary of State APIblog main image

Executive Summary: SOS API rate limits behave differently from the requests-per-second quotas most integration teams expect, because a live Secretary of State lookup is bound to a state registry that can absorb only so much traffic at once. Cobalt's endpoint throttles with HTTP 429 only on extreme spikes, queues normal bursts, and returns a 202 Incomplete with a retryId when a state takes longer than about 20 seconds: two signals that need two different responses. This article explains why the constraint sits at each state and how to build per-state queues, retries, callbacks, and cache fallbacks so one slow state never stalls the pipeline.

Why Does an SOS API Throttle Live Lookups Per State?

What is the scarce resource behind a live lookup?

A live lookup is not a database read. By default Cobalt fetches the record directly from the Secretary of State site at request time.[1] Each state runs its own portal, and Cobalt's help center says site design and load vary by state: Delaware, for example, loads slowly and uses CAPTCHAs, which extends response times.[2] The help center also says slower states such as Oregon and Delaware may need up to five minutes for a live result.[3]

Cobalt's own measurement on September 11, 2026 recorded far shorter times for those two states. It searched three large national companies by name in all 50 states and the District of Columbia: 153 probes, each with a 180-second ceiling. The 110 probes that returned a live record took 1.51 to 170.27 seconds, median 15.01; Delaware's took 13 to 20 seconds. In Oregon, one probe returned a record in about 30 seconds, and the other two completed with no exact match in about 15 and 17 seconds. Three Nevada probes were answered from the cache while the state site was down for maintenance, 19 in all completed with no exact match, and 21 did not finish inside the ceiling. That is one day and three well-formed names, not a service expectation, so hard-code neither figure. The bottleneck is the state portal, and any vendor that fetches live must decide how hard to press each one.

What has Cobalt said about per-state concurrency?

On a 2026 demo call, a prospect's developer asked what the rate limits were. Cobalt described a limit of two concurrent requests per state per customer, with anything above that queued, framed as respecting state resources and protecting against DDoS-like behavior. That is a call statement, not a published limit, and it did not distinguish live from cached requests. This article uses it to illustrate the limit's shape, not its value; confirm the current behavior with Cobalt before relying on it.

The public documentation describes the same queue-first behavior without a per-state number. Cobalt's help center says normal bursts of five to fifty or more requests are queued and processed steadily, while the endpoint throttles with HTTP 429 only on extreme spikes, such as hundreds or thousands of requests in a second.[4] Another entry says the application still queues and works through requests once traffic normalizes.[5]

The same developer asked whether requests that kept getting queued were theirs to resolve. Cobalt works through its queue, but the consequences land on the client: exhausted threads, timeouts, duplicate submissions, and missed SLA promises. The client needs its own queue, shaped per state.

How is a concurrency limit different from a rate limit?

A rate limit caps how many requests you may start in a window; a concurrency limit caps how many may be in flight at once. Long-running requests make the two diverge, and Stripe's documentation notes that issuing many long-lived requests can trigger concurrency limiting.[6] Live SOS lookups are the long-lived case: a 60-second lookup holds a slot thirty times as long as a two-second one, so a global requests-per-second figure says little about what a state lane can drain. Size lanes from latency, not from a rate.

What Are the Two Different Signals the Client Must Tell Apart?

What does HTTP 429 from the endpoint mean?

RFC 6585 defines 429 Too Many Requests as the status for a user who has sent too many requests in a given amount of time, and says the response MAY include a Retry-After header indicating how long to wait.[7] Cobalt documents two kinds of 429 on its SOS search. The endpoint-level throttle,[5] which fires on the extreme spikes described above, is a client pacing failure, not a state failure: stop dispatching, back off, and resume at a lower rate. A trial key that has used its allowance also gets a 429, with the message "Trial limit exceeded,"[8] and no backoff clears it, so branch on the message first.

Cobalt's public documentation does not describe a Retry-After value on its 429, so honor the header if present and use your own schedule if not. Do not expect RateLimit headers either: as of September 24, 2026, the IETF specification for them is still an Internet-Draft, version 11 from May 2026.[9] Keep your own counters.

What does a 202 Incomplete with a retryId mean?

If a lookup runs longer than about 20 seconds, Cobalt's help center says, the API returns a retryId instead of holding the connection open, and you poll with it until the request completes.[10] The public API reference puts it as some requests taking longer than 30 seconds,[8] so treat the window as a setting, not a constant. The response carries status Incomplete, statusCode 202, the retryId, and a message that the state is taking longer, and Cobalt states that this does not mean the lookup failed.[11] A 202 must never enter the retry path: re-submitting creates a second live lookup in the same lane and pushes the lane deeper into queueing.

Which other statuses change what the queue does?

Cobalt's status reference covers the rest: Complete, bad request messages, empty cached results, a no-exact-match message with alternativeResults, a fallback to recent cached data when the live search fails, 500 Failed, and a retryId that does not exist, has expired, or is no longer available.[12] A Bad request status is not always an input error. The status reference files the usage-cap message under Bad Request,[12] and the API reference gives the trial-limit 429 the status Bad request.[8] Both are account conditions, so read the message before routing, and stop dispatch and alert rather than retry into either.

How Should the Client Model Per-State Concurrency and Throughput?

Why does a single global queue stall?

Most thread-per-request servers use a queue in front of a thread pool and usually reject new requests once the queue is full.[13] Put SOS lookups through one global pool and you get head-of-line blocking: eight workers pick up eight Illinois jobs that each wait a minute or more, while jobs for faster states sit behind them. If the vendor also caps in-flight requests per state, most of those workers hold jobs already queued at Cobalt, so the pool looks busy while throughput collapses.

The Azure Architecture Center's Queue-Based Load Leveling pattern uses a queue as a buffer between a task and the service it invokes, to smooth intermittent heavy loads.[14] For SOS work the buffer must be partitioned by state, because the service behind it is 51 services with 51 speeds. One lane per state, each with its own in-flight cap, is the smallest design that stops a slow state from starving a fast one.

How do you estimate a lane's drain rate?

A lane's sustainable throughput is its allowed concurrency divided by the mean time a lookup holds a slot. With two lookups in flight, the figure Cobalt described on the call, a state averaging 60 seconds drains about two jobs a minute, roughly 120 an hour; at five seconds it drains about 24 a minute. That is arithmetic on an unconfirmed ceiling, not a throughput promise.

For starting hold times, Cobalt's help center publishes response-time bands: cached lookups 1 to 3 seconds, often under a second; fast live states about 7 to 15 seconds; slower states up to two minutes.[15] It also says Oregon and Delaware may need up to five minutes.[3] Those are the help center's figures, not guarantees; in Cobalt's one-day measurement above, live records took more than two minutes in four states.

Measure rather than assume. Record per-state completion times from your own traffic, keep a rolling median and ninetieth percentile, and recompute each lane's drain rate from them. Cobalt's batch guidance does not prescribe those statistics; it tells you to expect variance by state and names Illinois as a slower one.[16]

What should the queue record for every job?

The job record lives in your system and must survive a worker crash, a duplicate callback, and a late poll. The example is illustrative, not a Cobalt API schema.

{
  "job_id": "verif-20260924-000418",
  "lane": "il",
  "mode": "live",
  "input": { "searchQuery": "Example Business LLC", "state": "il", "screenshot": true },
  "dedupe_key": "il:name:example business llc:live:2026-09-24",
  "status": "awaiting_callback",
  "attempt": 2,
  "max_attempts": 4,
  "submitted_at": "2026-09-24T14:02:11Z",
  "cobalt_request_id": "req-example-7f3a",
  "retry_id": null,
  "next_action_at": null,
  "history": [
    { "at": "2026-09-24T14:01:40Z", "event": "http_429", "backoff_ms": 1830 },
    { "at": "2026-09-24T14:02:11Z", "event": "submitted", "note": "callbackUrl set" }
  ]
}

Two fields do the heavy lifting. The dedupe key stops a retry or re-run from becoming a second live lookup. The request identifier ties a callback to its job, because Cobalt's callback POST carries the requestId it returned at submission.[11]

How Should Retries Be Designed So They Do Not Make the Queue Worse?

Which responses are retried and which are not?

• HTTP 429. If the message reads Trial limit exceeded, stop and alert.[8] Otherwise back off with jitter, pause dispatch across every lane, and resume at a lower rate.[5]

• 202 Incomplete with retryId. Not a retry. Keep the job in flight and collect the result by poll or callback.[11]

• 500 Failed with no records. Enter bounded retry with backoff; the lookup did not complete, so a second attempt is legitimate.[12]

• Bad request. If the message reads Usage cap exceeded[12] or Trial limit exceeded,[8] stop dispatch and alert. Otherwise reject the job and surface the input error; a retry returns the same message.[12]

• No exact match. Complete the job and pass alternativeResults to your matching rules.[12]

• Fallback to cached results. A live request that returns records in results alongside a top-level message was answered from cache, whatever the wording;[8] the one exception is the Business found note the status reference lists for success.[12] Complete the job, flag the provenance as cached, check the records against your matching rules because some fallbacks return other businesses, and let your freshness rule decide on a later live re-run.

• Retry ID invalid or expired. Resubmit under the same dedupe key and log the expiry as a poller failure.[12]

The retry logic and graceful failure guide covers state failure modes and graceful-failure options in depth; this section adds the per-lane consequence of each choice.

What backoff, jitter, and budget policy fits a per-state lane?

Always use randomized exponential backoff when scheduling retries.[13]

That rule sits in a list of retry considerations in Google's Site Reliability Engineering book. The same list says to limit retries per request and to consider a server-wide retry budget, such as 60 retries per minute in a process, beyond which you fail the request instead.[13] AWS's analysis found that Full Jitter substantially decreases both client work and server load compared with un-jittered exponential backoff.[17]

Apply the budget per lane and per account. A per-lane budget stops one sick state from spending the system's retry allowance; an account-level budget stops the sum of the lanes from tripping the endpoint throttle. Google's client-side throttling goes further: a client that sees a significant share of recent requests rejected caps its own outgoing traffic, and requests above the cap fail locally without reaching the network.[18]

How do you keep a retry from creating a duplicate lookup?

Stripe's API accepts an idempotency key so a client can safely retry a request without performing the operation twice.[19] Cobalt's public documentation describes no equivalent header, so the dedupe lives on your side, and Cobalt's batch guidance tells you to design queues with backoff and idempotency.[16] The practical key is state plus the normalized search term plus mode plus a freshness window, as in the job record above; before any submission, the dispatcher looks for an existing job with that key and attaches to it. An ID search would make a firmer key, because two spellings of one entity's name would collapse to one identifier, but Cobalt's sources disagree on where it works. The API reference documents sosId as a general search input,[8] and a help-center answer on batch uploads says a state plus registration ID scopes the lookup to that specific record.[20] Cobalt's public Fields Available by State spreadsheet, however, marks its searchBySosId row for only Florida and Mississippi.[21] Check the spreadsheet for the state, and confirm with Cobalt, before keying searches on an ID. Persist the retryId or requestId the moment it arrives, so a crash resumes rather than resubmits.

When Should a Lookup Go to Cache Instead of Waiting in a Live Lane?

What does liveData=false change?

Cobalt keeps its cache as a fallback for when a state site is slow or down.[1] A request with liveData=false is served from that cache for a much faster response, which Cobalt suggests for large batches on a tight SLA.[22] The trade is coverage and freshness: cached coverage varies by state, and the data may be outdated.[23] Cached records do not auto-expire; each payload carries createdAt and updatedAt so you can judge freshness yourself.[24] A miss is not evidence of absence either: it means only that the entity is not in cache, not that it does not exist at the state.[25]

How does a cache-first waterfall relieve live lanes?

Cobalt's batch guidance is to start with liveData=false for breadth when speed matters more than recency, then re-run the no-hits live.[16] Only misses and records that fail your freshness rule then enter a live lane, so the live lanes carry a fraction of the volume. Build the waterfall as two queues, not one flag. The cache queue is not bound by state latency and can run wider than any live lane. It still hits the same endpoint, though, and Cobalt describes the 429 throttle at that level without exempting cached requests,[5] so keep the cache queue inside the account-level budget. Whether the per-state concurrency described on the call covers cached requests is not published; confirm it before sizing the cache queue. The interim cached data guide covers the cached record Cobalt can return while a live lookup is still running.

What must not be delegated to cache?

Whatever your rules say requires a current registry state, only a live lookup supplies it, and Cobalt notes that live lookups enable timestamped screenshots of the source page.[26] If a decision needs that artifact, the job belongs in a live lane however long the wait. Cobalt supplies the record, its source, and its timestamps; how old is too old is your underwriting policy.

How Do Callbacks Change the Queue Design Compared With Polling?

Why does Cobalt recommend callbackUrl for batch and high-volume work?

Cobalt supports both patterns and recommends callback for batch or high-volume use.[10] Its batch guidance gives the mechanical reason: prefer callbackUrl so long-running states do not tie up your HTTP threads, and use retryId polling if you do not have webhooks yet.[16] With a callback, the initial response carries only a requestId, never results, and the completed payload is POSTed to your endpoint when ready.[11] A polling worker holds capacity for the life of the lookup, so a slow lane consumes workers in proportion to its latency. A callback lane submits, records the requestId, and releases the worker, so the in-flight set is rows in a table. The async webhook architecture guide covers callback security and missing callbacks; here you tune the lane cap, not the thread count.

What does a polling loop need to get right?

If you poll, send the retryId as a parameter on the same /v1/search request until it completes, and include screenshot=true on every retryId request when the job needs the screenshot, as the API reference directs.[8] Cobalt's help center says retry results remain available for three days,[27] which is the deadline for a restarted poller to resume. The loop needs three guards: a floor on its interval, so a busy lane does not become its own burst; a ceiling on polls per job, so a stuck lookup is escalated; and a rule that treats a retry-invalid response as a fresh submission.[12] Match each lane's interval to its median latency.

How should the callback receiver be built?

The receiver should acknowledge quickly, persist the payload before doing anything else, and match the incoming requestId to a known job, since Cobalt includes the same requestId in the POST.[11] Unmatched payloads go to a holding queue; duplicate deliveries for a completed job are logged and ignored. Store the screenshot itself, not only its link: the API reference describes it as a short-lived URL,[8] so download the file when the payload arrives.

What Happens When a State Is Down or a Lane Stalls?

How do you detect a stalled lane?

Three signals together separate a slow state from a broken one. The first is the age of the oldest in-flight job against the lane's rolling ninetieth percentile. The second is the share of recent submissions returning 202 instead of completing inside the synchronous window. The third is the share of live requests answered from cache, counted by the top-level message rule in the retry section. The API reference lists several wordings for that message,[8] and all three Nevada probes in Cobalt's September 11 measurement carried this one: "This state is currently down for maintenance. You have not been charged. However, here are some results from our database that match your search." Treat the wordings as one family, never one exact string. Cobalt's help center adds that it does not differentiate between an issue at the state site and one on Cobalt's side,[28] so treat the lane as down either way.

Wrap each lane in a circuit breaker. In the pattern Michael Nygard popularized and Martin Fowler documented, a wrapper monitors a protected call for failures and, once they reach a threshold, trips so further calls return an error without the protected call being made.[29] For a state lane, tripped means no new live lookups to that state, continued collection of results already in flight, and a single probe lookup on a timer.

What should a tripped lane do with waiting jobs?

Park them; do not drop them. If your rules allow, answer a parked job from cache with the provenance flag set and schedule a live follow-up for when the breaker closes; if not, the job waits and its completion estimate reflects the outage. Amazon SQS moves a message to a dead-letter queue after a consumer has received it a configured number of times, isolating unconsumed messages so you can determine why processing did not succeed.[30] The SOS equivalent is a per-lane dead-letter list with the full attempt history, reviewed by a person.

How do you talk to the business about a state outage?

Quote expectations per state, from your own data. Give the business each lane's rolling ninetieth percentile as its completion estimate, and when a breaker trips, name the parked state and the time of the next probe. The SOS API reliability guide explains how Cobalt behaves during portal outages.

That completes the client side. What remains is the data source, and that choice decides how much of the per-state problem your engineers carry.

Where Does Cobalt Fit in a Queue-Aware Integration?

Every section so far applies to any source of live Secretary of State data. Building your own scrapers means owning 51 portals, their CAPTCHAs, and a politeness limit for each. An aggregated secondary database sidesteps the live problem, at the cost of records only as current as its last refresh. Full-service verification platforms bundle the data with their own decision rules. A direct data API sits between those options: the live problem remains, but the vendor carries the per-portal mechanics and gives the client one continuation model to queue against.

What does Cobalt supply and what stays with the client?

Cobalt's side is the set of mechanics cited above:

• Live coverage. Lookups in all 50 states and the District of Columbia.[3]

• Cached mode. liveData=false when speed matters more than recency.[22]

• Continuation. A retryId when a lookup outlasts the synchronous window.[10]

• Callbacks. A requestId at once and a POST of the finished results later.[11]

• Throttling. Queuing for normal bursts, and an endpoint throttle that returns 429 only on extreme spikes.[4]

• Status vocabulary. A documented set of statuses and messages.[12]

The Top 34 questions about Secretary of State APIs hub collects the broader buyer questions around these mechanics.

The limitations belong in the same picture. Live latency varies by state and can run past the synchronous window by minutes. Cached data may be outdated, its coverage varies by state, and a miss does not prove an entity is absent. Field availability, officers included, differs by state. Screenshot URLs are short-lived. The per-state concurrency figure from the call is unpublished, so confirm lane caps with Cobalt rather than hard-coding them from this article. For security questions, request Cobalt's current security documentation. Cobalt is a data source, not a decisioning engine: lane sizing, retry budgets, dedupe rules, freshness thresholds, and the accept or reject decision stay in your code.

Which questions belong in an integration review?

Bring the numbers you now have: expected volume by state, the share that must be live, the callback endpoint you will run, the freshness rule that decides when cache may stand in, and your retry budget. Ask Cobalt to confirm the current per-state concurrency, whether cached requests count against it, and what its throttle 429 carries. To walk through your queue model against the current API behavior, schedule a demo framed as a workflow and integration review. Then build the lanes, measure them for a week, and resize them from your own data.

References

1. Does Cobalt provide real-time Secretary of State data or is it cached?, Cobalt Intelligence

2. Why do some states take longer or fail more often?, Cobalt Intelligence

3. Can Cobalt retrieve live data from all 50 states + DC?, Cobalt Intelligence

4. What happens if we accidentally send a very large burst of requests?, Cobalt Intelligence

5. Does the 429 come from the app or the endpoint?, Cobalt Intelligence

6. Rate limits, Stripe

7. RFC 6585: Additional HTTP Status Codes, M. Nottingham and R. Fielding

8. Secretary of State API, Cobalt Intelligence

9. RateLimit header fields for HTTP (draft-ietf-httpapi-ratelimit-headers), IETF HTTPAPI Working Group

10. What happens if a lookup takes too long to return a response? How do retryId and callbackUrl work?, Cobalt Intelligence

11. How should we handle long-running SOS searches? What does Incomplete mean?, Cobalt Intelligence

12. What do the different API response statuses and messages mean?, Cobalt Intelligence

13. Addressing Cascading Failures, Google

14. Queue-Based Load Leveling pattern, Microsoft

15. Why are some Secretary of State requests slow or inconsistent, and how long do results take?, Cobalt Intelligence

16. Any guidance for large batch runs?, Cobalt Intelligence

17. Exponential Backoff And Jitter, Marc Brooker

18. Handling Overload, Google

19. Idempotent requests, Stripe

20. If I provide entity name, state, and registration number in a batch upload, will the API only check those exact entities?, Cobalt Intelligence

21. Fields Available by State, v05272026, Cobalt Intelligence

22. Can I choose faster responses over absolute freshness?, Cobalt Intelligence

23. Should I query cached results first or always use live data?, Cobalt Intelligence

24. How long is cached data stored or considered valid?, Cobalt Intelligence

25. What happens if a cached lookup doesn't find a record?, Cobalt Intelligence

26. Are results live or from a cache by default?, Cobalt Intelligence

27. What happens if I never pull results for a retryId? Will I still be billed?, Cobalt Intelligence

28. How can we identify if a failure is due to the state vs. Cobalt?, Cobalt Intelligence

29. CircuitBreaker, Martin Fowler

30. Using dead-letter queues in Amazon SQS, Amazon Web Services