An SDK example works in a notebook. The researcher moves it into a scheduled job, downloads three pages, and loses the connection on the fourth. The next morning, they need to know which records were saved and where to resume.

The notebook has become part of the researcher’s application. They now need to stop it, diagnose a failure, and restart without mixing old and new data. Those requirements should shape the SDK before it grows a large collection of endpoint wrappers.

A research team sees this in its own jobs before anyone names it: rows duplicated after a job restarted, or retry and checkpoint code wrapped around the SDK by whoever owns the job. Each is a sign that the convenient path hides the controls a job needs when it fails.

The same gap shows up in the official Python SDKs of two trading venues, in opposite ways. Polymarket’s SDK, at version 0.13.0, reconnects a dropped market stream and resubscribes without telling the application, which keeps reading with no sign that updates were lost in the gap. Another user has asked for a way to detect this (py-sdk #307), and I added measurements from my own runs there. Binance’s SDK, at version 13.2.0, reports the drop. But if the application opens a new connection on the same client and subscribes to the same stream again, the SDK skips any stream it still has on record. It sends nothing and raises nothing, and the silent stream can pass for a quiet market. Before 13.2.0, a new client in the same process skipped the stream too. I’ve reported the remaining case (#576).

The Polymarket behavior was observed on its live market stream with version 0.12.0 on October 4, 2026 (findings), and again with 0.13.0 on October 16 (recheck). The Binance behavior comes from reading the SDK’s source and running it against a local test server, not from Binance’s live service. Binance has been changing its SDK’s failure handling quickly. Since July it has added connection callbacks, a reconnect event, and retry timing on rate-limit errors. On October 6 it fixed server errors that were never retried, and on October 15 it gave each client its own record of streams (changelog). A later release may behave differently.

The convenient path should keep those controls within reach. An iterator can save a researcher from writing a pagination loop. A scheduled job may still need the individual pages and continuation tokens so it can commit its progress. Both should express the same query and return records with the same meaning.

I’ve written SDKs for a crypto exchange and a prediction market, in each case because the official SDK had problems in testing or didn’t expose data the API could.

Take a researcher who wants a local copy of an economic series as it was available on September 4, 2026. The data here is invented for illustration: the August activity index is released as 102.4 on September 3, then revised to 102.1 on September 10. The September 4 download must contain 102.4.

Start with one month so the answer is easy to check. The snippet illustrates the shape of interface I aim for; Client stands in for the SDK’s entry point.

import os

with Client(api_key=os.environ["FINANCIAL_DATA_API_KEY"]) as client:
    observations = client.observations.iterate(
        series_id="activity-index",
        period_start="2026-08-01",
        period_end="2026-09-01",
        available_as_of="2026-09-04T00:00:00Z",
    )
    for observation in observations:
        print(observation.period_start, observation.value, observation.revision_id)

The period selects August, with September 1 excluded. available_as_of asks which version the provider could deliver by the cutoff. Without available_as_of, rerunning the query after September 10 could return the revision. The record type needs to retain the revision identity and availability time so the researcher can tell which value they used.

For this index, a decimal value and an explicit unit avoid leaving numeric interpretation to the caller. A missing value stays missing. A dataframe helper that turns the result into just date and value would discard the revision information the query was designed to preserve. Any such reduction belongs in the customer’s analysis, where they can choose it deliberately.

The caller should control how much work runs at once

For a notebook workflow like this one, I start with synchronous calls and an iterator that fetches a page when the caller needs it. The caller determines how quickly to consume records. The SDK starts no background prefetch task, and the with block makes connection cleanup explicit if the loop fails or the researcher stops early.

My preference for synchronous calls comes from Go library development. Go’s guidance on synchronous functions recommends keeping work within the lifetime of a call and letting the caller add concurrency. A synchronous method accepting a context for cancellation fits that model; the application can call it from a goroutine when needed.

The same principle needs a different interface in other languages. A JavaScript network client should use native promises, as the Azure TypeScript SDK guidelines recommend. When the applications calling it are async, add an async client alongside the synchronous one. The Azure Python SDK guidelines use that separation too. Awaiting each request in a loop keeps those requests sequential; an async interface still lets the caller decide which operations run concurrently.

An optional bulk-download helper might justify parallel requests or prefetching. That helper needs explicit limits on requests and buffered data, a way to cancel, and a documented point at which its background work has stopped. Customers can then choose the throughput benefit with an understanding of its resource costs and cleanup requirements.

A failed fourth page must leave a recoverable download

Extend that query back to January 2024 and a single response no longer holds all the observations: at ten per page, the 32 months span four pages. An iterator keeps the notebook convenient, but fetching the next item can now make a network request. The fourth-page failure arrives inside the loop, after the caller has already done work with earlier records. The iterator’s documentation needs to explain that fetching a new page can wait, retry, or fail.

The scheduled job needs the pages and their continuation tokens, not only the individual observations. It saves each page’s records and the token that follows them in one local transaction:

import os
import sqlite3
import sys

JOB = "activity-index-2026-09-04"
db = sqlite3.connect("research.db")  # observations are keyed on revision_id
saved = db.execute(
    "SELECT page_token, complete FROM checkpoint WHERE job = ?", (JOB,)
).fetchone()
if saved and saved[1]:
    sys.exit(0)  # the download already finished

with Client(api_key=os.environ["FINANCIAL_DATA_API_KEY"]) as client:
    pages = client.observations.pages(
        series_id="activity-index",
        period_start="2024-01-01",
        period_end="2026-09-01",
        available_as_of="2026-09-04T00:00:00Z",
        page_size=10,
        page_token=saved[0] if saved else None,
    )
    for page in pages:
        with db:  # the page's records and the token after them commit together
            db.executemany(
                "INSERT OR IGNORE INTO observations VALUES (?, ?, ?, ?)",
                [(o.revision_id, o.observation_id, o.period_start, str(o.value))
                 for o in page.records],
            )
            db.execute(
                "INSERT OR REPLACE INTO checkpoint VALUES (?, ?, ?)",
                (JOB, page.next_page_token, page.next_page_token is None),
            )

A crash before a page commits leaves the previous checkpoint intact, and the job fetches that page again. A crash after the commit resumes at the next page. Because the table is keyed on revision identity, a page fetched twice changes nothing. Reverse the order and the job breaks: saving the token before the records skips data after a crash, and saving records without a replay-safe key duplicates them. The SDK’s part is to expose the page boundary clearly enough for the application to choose that transaction, and to document this job alongside the notebook.

Tokens don’t last forever. When the snapshot behind a saved token has expired, the SDK should raise a distinct error so the job can clear its checkpoint and restart the query. With a past cutoff such as September 4, the restarted query returns the same records, provided the API preserves its history, and the key absorbs the repeats. A query for current data is different: a revision can land between page three and page four, so its pages are consistent only if every token refers to a snapshot the server retains. A more careful client loop can’t supply that guarantee; the server has to.

Some interruptions can be handled within a request. A transient connection failure may permit another read attempt. An invalid query needs the caller to change it. If the job has thirty seconds left and the server asks it to wait sixty, the client should return the throttling error and its retry information. HTTP’s Retry-After accepts either a date or a delay in seconds, so both forms need handling.

A finite attempt limit and total retry budget keep a scheduled job from retrying into its next run. Within those limits, backoff with jitter spreads retry attempts. The caller’s overall deadline needs to cover requests and retry waits, and cancellation needs to interrupt both. A method that creates or changes remote state would need a separate guarantee that repeating it cannot duplicate the effect. When the SDK gives up, a provider request ID in the error helps support find the failed attempt.

Now change the failure: the customer’s credential no longer grants access to the series. Returning an empty iterator would make the job look successful. An access error has to remain distinguishable from a query that matched no observations, and from invalid credentials. The error and logs must omit the secret itself.

The shipped package has to run the customer’s example

Before shipping, build the distribution and install it in a clean environment outside the source tree. Running the notebook and the scheduled job there catches packaging problems that importing from a checkout can hide. Run the same examples on every supported Python version, and keep dataframe dependencies optional for customers who only need the client.

The package also needs to fit into the application around it. Let customers supply a transport for their connection requirements or their own local tests, with clear ownership of who closes it. Logging should use the language’s usual facilities without installing global handlers or changing the application’s log level. These integration points let customers reuse their infrastructure while keeping the default setup small.

Test recovery through deliberate interruptions. Stop the job after a committed page, resume it, and compare its stored records with expected results prepared separately. Repeat while a revision arrives, and again after the token has expired. Force a throttle response past the retry budget and revoke dataset access. Run those checks against a representative API or sandbox as well as local mocks, so the client is tested against behavior it didn’t define for itself.

A copy pinned to September 4 never changes. Keeping a current copy means following delivered changes instead, because asking for months after August would miss the revision to August itself. The SDK needs an update cursor that follows revisions and withdrawals, with an example that applies them without losing the versions earlier research used.

The installed examples then become compatibility checks for future SDK releases. That commitment includes behavior such as ordering, retry defaults, and snapshot selection as well as method signatures. When an API or SDK change alters that behavior, the release needs a migration example showing what the customer must change and what happens to saved data.

When a vendor’s SDK leaves out the recovery a scheduled job needs, I write that part of the client and keep the SDK where it fits, as part of the API integrations I build. If one of your jobs can’t tell what it saved after a failure, tell me about it.