ERCOT engineering guide · Reliability

ERCOT API timeouts: what we learned downloading a 655,389-record report

ERCOT Public API requests can be fast for hundreds of pages and unexpectedly slow for the next one. That makes timeout handling harder than simply choosing a bigger number.

Published by SteadyState Data · Updated

While building and testing an ERCOT Public API connector, we repeatedly retrieved a large ERCOT report containing 655,389 records across 33 pages. The testing exposed several behaviors that matter when building production ERCOT data pipelines:

  • a 30-second read timeout can be too short for large reports;
  • increasing the page size does not necessarily explain slow requests;
  • a particular page is not necessarily consistently slow;
  • retries can recover many slow requests, but not all of them;
  • we observed no HTTP 429 responses, so rate limiting was not visible in anything we measured — but the cause of the timeouts remains unknown;
  • bounded concurrent pagination completed the full report in our tests without buffering it in memory;
  • and, critically, HTTP read timeout is not the same thing as total request duration.

The result was a timeout strategy based on observed report metadata rather than assuming every ERCOT request needs an unusually large timeout.

Where this guide sits in the pipeline

  1. Source
  2. Fetch
  3. Raw copy
  4. Validate
  5. Verify completeness
  6. Normalize
  7. Promote
  8. Client system

The report we tested

Our large-report workload used ERCOT report NP4-215-CD, wruc_as_demand_curves, for September 24, 2026.

With a requested page size of 20,000 records, ERCOT reported:

  • 655,389 total records
  • 33 pages
  • 20,000 records on pages 1–32
  • 15,389 records on page 33

This is useful as a stress test because it is substantially larger than many routine ERCOT API queries.

For comparison, other reports we tested were much smaller. One test of NP4-745-CD returned 5,184 records across six pages, while a filtered NP4-190-CD query returned only 24 records on one page.

A timeout policy that makes sense for a 655,000-record query does not necessarily make sense for a 24-record query.

First lesson: a 30-second timeout is not always enough

Our original transport configuration used an HTTP timeout with a 30-second read timeout. That worked well for ordinary requests. It was less reliable on the large report.

During one serial retrieval of NP4-215-CD, the collector successfully persisted 560,000 records across 28 pages before page 29 exhausted its attempts and failed. Other pages had already required retries.

The important observation was not that “page 29 is slow.” In other runs, different pages became slow. The behavior was variable.

A later run starting at page 28 produced approximately:

PageObserved behavior
2815 seconds
29timeout, then success in 11 seconds
308 seconds
318 seconds
325 seconds
33two timeouts, then success in about 20 seconds

Then we requested page 33 separately. It timed out on all three attempts.

So the evidence did not support a simple rule such as “late pages are slow,” nor “page 33 always takes a long time.” The same page could behave very differently between requests.

A timeout is not evidence of rate limiting

When an API becomes slow, it is tempting to assume rate limiting. We specifically looked for that.

Across these large-report experiments, we did not observe HTTP 429 responses during the relevant runs. That matters.

A timeout and a rate-limit response are different failure modes and should be handled differently. A 429 Too Many Requests gives the client explicit information that request volume has exceeded a limit. A read timeout tells us only that the client waited too long for network data according to its configured timeout.

The timeout alone does not tell us why. It could involve upstream processing latency, network behavior, infrastructure load or other factors that are not visible to the client.

Our tests therefore do not establish that ERCOT has a database indexing problem, an SQL pagination problem, a response-size problem or an undocumented rate limit. They establish something narrower and more useful: response timing for large ERCOT API requests can vary enough that a normal 30-second read timeout may be insufficient.

Read timeout does not mean “the request may take only 30 seconds”

This distinction is easy to miss.

An HTTP client’s read timeout generally controls how long the client is willing to wait for incoming data during a read operation. It is not necessarily a stopwatch placed around the entire HTTP request.

That means this:

read timeout = 30 seconds

does not necessarily mean:

the entire request must complete within 30 seconds

This became especially important when interpreting our measurements. In one controlled run using a 120-second read timeout, some complete request/response attempts took more than 30 seconds. The longest took approximately 84 seconds from sending the request through receiving the complete response.

That does not prove that the connection went 84 seconds without receiving data. Therefore, it would be incorrect to say that the request “needed an 84-second read timeout.”

We know the total observed duration. We do not know the longest interval between bytes received by the HTTP client. Keeping those measurements separate prevents misleading conclusions when tuning a production collector.

Simply retrying at 30 seconds wasn’t enough

Retries helped substantially. They did not solve every case.

In our serial large-report run, 30-second read timeouts triggered retries on numerous pages. Some retries succeeded. Eventually one page exhausted the available attempts and the report stopped after 560,000 persisted records.

This illustrates why retries and timeouts need to be designed together. If every attempt has the same timeout:

30 seconds
30 seconds
30 seconds

then a request that repeatedly needs a longer read window may never get one.

But setting every ERCOT request to something like 120 seconds has its own downside: small requests that should fail quickly now have a much larger failure window.

The better question became: when do we have enough information to know that we’re retrieving a genuinely large report?

Let ERCOT’s pagination metadata inform the timeout

The ERCOT API provides pagination metadata after a report request. That creates a useful boundary.

Before receiving the first page, the client does not necessarily know how large the result will be. After the first response, it may know the total record count.

We therefore settled on this behavior:

Unknown report
    ↓
Request discovery/first page using caller's normal timeout
    ↓
Read ERCOT pagination metadata
    ↓
Is totalRecords >= 100,000?
    ├── No  → preserve caller timeout
    └── Yes → use at least a 120-second read timeout
              for subsequent pages

The threshold and timeout are engineering defaults derived from our testing, not ERCOT-published requirements. That distinction is important.

We are not saying ERCOT reports containing 100,000 records require a 120-second timeout. We are saying that once our collector has evidence that it is dealing with a large result, it becomes reasonable to give subsequent reads more room without penalizing every normal ERCOT request.

These tests do not establish where that threshold belongs. They show that a larger read-timeout floor helped the workload we measured; 100,000 records is the point we chose to switch behavior, not a point ERCOT documents or our data identifies.

Preserve the caller’s intent

The automatic policy should never make a caller’s timeout more restrictive. For a known-large report:

Caller read timeoutEffective read timeout
30 seconds120 seconds
60 seconds120 seconds
90 seconds120 seconds
119 seconds120 seconds
120 seconds120 seconds
150 seconds150 seconds
unlimitedunlimited

Only the read timeout changes. Connect, write and connection-pool timeouts remain untouched. The policy can also be disabled when an application needs complete control over its transport behavior.

Why the first page remains different

There is an unavoidable limitation to a metadata-driven policy. Suppose the client requests:

page=1

It doesn’t yet know that the result contains 655,389 records. The first request therefore uses the normal caller-configured timeout. Only after ERCOT returns metadata can the collector classify the result as large.

The same issue occurs if someone starts directly at a later page:

page=33

If that request is made without previously retrieving report metadata, the fact that it says “page 33” does not prove anything about the total result size. Guessing would make the timeout policy unpredictable.

So we don’t guess. The first requested page remains subject to the caller’s timeout until actual report metadata establishes the result size.

Smaller pages did not eliminate the problem

Another seemingly obvious solution is to reduce the page size. We tested that too.

At a 20,000-record page size, one streaming run reached page 12 before ultimately failing after retries. We then cut the page size to 10,000 records. That run reached 140,000 persisted records before page 15 exhausted its attempts.

This does not prove that page size has no effect on ERCOT response time. It does show that simply halving the requested page size did not eliminate the timeout behavior we were investigating.

That is an important distinction. We don’t have enough evidence to claim a causal relationship between response size and latency in either direction.

Streaming is safer than collecting a giant report in memory

Timeout behavior also changed how we think about the application architecture around large ERCOT reports.

For a small report, this is convenient:

result = await client.get_report(...)

The application receives the complete result. For a 655,389-record report, a streaming model is generally more useful:

async for page in client.iter_report(...):
    persist(page)

Each successfully validated page can be persisted immediately. If page 29 eventually fails, pages 1–28 do not need to disappear with it.

This does not mean the connector itself needs to own checkpoint persistence. The caller may be writing to PostgreSQL, object storage, Parquet, CSV or another system entirely. The connector’s job is to expose validated pages cleanly enough that the application can decide how durability works.

The important architectural rule is: don’t make successful retrieval of hundreds of thousands of records depend on keeping the entire report in memory until the final page succeeds.

Concurrency needs a boundary

Once the first page tells us how many pages exist, later pages can potentially be retrieved concurrently. But launching 32 requests simultaneously would create a different set of problems.

We implemented bounded, ordered concurrency instead. Conceptually:

page 1 → discover report size

then, with max_in_flight=2:

page 2 ────────┐
page 3 ────────┤
               ↓
yield in page order

page 4 ────────┐
page 5 ────────┤
               ↓
...

The number of pages launched but not yet yielded is bounded. Completed pages can wait briefly for an earlier page, but the collector does not create tasks for the entire report or buffer the entire result.

In our live test with max_in_flight=2, the 33-page, 655,389-record workload completed successfully in about 522 seconds. There were seven timeout retries during that run. There were no observed HTTP 429 responses.

This test demonstrated that bounded concurrency can operate successfully with the retry, authentication and request-coordination machinery. It does not establish that two concurrent requests are universally optimal for ERCOT.

A separate 120-second run told us something else

We also ran the same 33-page workload with a caller-configured 120-second read timeout and max_in_flight=2. That run completed in approximately 396 seconds with:

  • 33 pages retrieved;
  • 655,389 records retrieved;
  • zero retries;
  • zero observed HTTP 429 responses;
  • and a longest complete request/response attempt of approximately 84 seconds.

This was useful evidence in choosing a larger timeout floor for known-large reports. But there is an important caveat.

That test used the 120-second timeout beginning with page 1. Our eventual automatic policy does not. The automatic policy lets the discovery page use the caller’s normal timeout and applies the larger floor only after metadata identifies the report as large.

So we have tested the components and workloads behind the policy, but we should not claim that this exact automatic policy has been validated end-to-end against ERCOT.

What we still don’t know

Good production engineering requires keeping observations separate from explanations.

Our testing does not tell us why some ERCOT requests took longer than others. We did not establish that the behavior was caused by:

  • SQL OFFSET pagination;
  • missing database indexes;
  • ERCOT server load;
  • response payload size;
  • network congestion;
  • an undocumented throttle;
  • a particular report page;
  • or any other specific backend mechanism.

Those may be hypotheses worth investigating, but they are not findings. What we actually observed is enough to design around the problem without pretending to know its internal cause.

Practical recommendations for ERCOT collectors

For production ERCOT API pipelines, our current approach is:

  1. Use a reasonable normal timeout. Don’t give every request an enormous timeout because some reports are large.
  2. Treat read timeout separately from total request duration. They measure different things.
  3. Use ERCOT’s returned metadata. Once the API tells you a result is large, the collector can adjust accordingly.
  4. Retry transient read failures, but keep retries bounded. Infinite retries turn a data problem into a stuck pipeline.
  5. Keep rate-limit handling separate from timeout handling. A 429 is explicit rate-limit information; a timeout is not.
  6. Stream large reports page by page. Validate and persist successful pages instead of requiring the whole report to live in memory.
  7. Bound concurrency. A small number of concurrent page requests can improve throughput without creating an uncontrolled request burst.
  8. Preserve ordering. Concurrent retrieval doesn’t mean downstream consumers should receive pages unpredictably.
  9. Log attempts, retries, timeout configuration, page numbers and report metadata. Without those measurements, timeout tuning becomes guesswork.
  10. Don’t infer an ERCOT backend problem from client-side symptoms. Engineer around what you can observe.

Practical takeaway

The most useful result from this testing wasn’t discovering that “ERCOT needs a 120-second timeout.” That statement would be too broad. The useful result was discovering that timeout policy can be data-aware.

  • Keep a normal timeout for normal requests; raise the floor only on evidence.
  • Read ERCOT’s pagination metadata before deciding how patient to be.
  • Never make a caller’s timeout more restrictive than they asked for.
  • Treat a read timeout and a 429 as different failure modes.
  • Persist each validated page, so a late failure doesn’t discard the early ones.
  • Bound concurrency and preserve page order.
  • Report what you measured, not what you assume caused it.

Small ERCOT requests can retain normal failure behavior. Once ERCOT itself tells us that a result contains hundreds of thousands of records, the collector can become more tolerant while remaining bounded.

Combine that with page-level persistence, bounded retries, request coordination and limited concurrency, and an occasional slow response becomes an operational condition rather than a reason to restart an enormous download from scratch.

That’s the difference between a script that can call the ERCOT API and a collector designed to keep running when the API doesn’t behave exactly the same way every time.

Sources & further reading

The measurements in this guide are our own, from connector testing against the ERCOT Public API on September 24, 2026. They describe what we observed, not ERCOT-published behavior, and ERCOT APIs can change — verify current requirements against ERCOT’s official documentation before implementing production systems.

SteadyState Data

Need this running reliably?

SteadyState Data builds and operates focused power-market data pipelines for trading, analytics and energy teams. Start with the market, dataset and destination. We can help determine the smallest reliable system required to collect, validate and deliver it.

Discuss an Energy Data Project