> For the full page index, fetch https://echonova.ai/llms.txt

# Latency best practices

How to get results back sooner and waste fewer requests, using the sync and async endpoints, the rate limit, the job timeout and the webhooks as they behave.

The time between your request and a usable result is spent in four places:
your connection to the API, the work itself, the wait between the work
finishing and you learning about it, and the download of the result. The
API gives you a choice at each step. This page explains which choice is
faster and why, using the API's limits and timings.

## Pick the call style by size, not by habit

Each service has a synchronous endpoint that holds the connection until the
result is ready, and an asynchronous one that answers at once with a
`jobId`. The synchronous call is the shortest path for one result: one
request, one response, no second call. It is capped by input size.

| Endpoint                       | Cap on input                | What you get back                                                                                       |
| ------------------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------- |
| `POST /v1/text-to-speech/sync` | 500 characters of `text`    | `jobId`, `status`, `audioUrl`, `durationSeconds`, `creditsConsumed`                                     |
| `POST /v1/speech-to-text/sync` | 10 minutes of audio, 150 MB | `jobId`, `status`, `transcription`, `durationSeconds`, `language`, `numbersAsDigits`, `creditsConsumed` |
| `POST /v1/text-to-speech`      | 500 characters of `text`    | `jobId` and `status`                                                                                    |
| `POST /v1/speech-to-text`      | 10 minutes of audio, 150 MB | `jobId` and `status`                                                                                    |

Both Text to Speech endpoints share the 500-character cap, and both Speech
to Text endpoints share the 10-minute cap, so the asynchronous one does not
buy you longer input. What it buys is a free
connection: your process is not held open while the work runs, and a
long transcription cannot hit a timeout in your own HTTP stack. Use the
synchronous call when you need the result in the same request, for example
to answer a user who is waiting. Use the asynchronous call when the result
can arrive later, or when you have many items to process. The two styles are compared in full on
[Synchronous and asynchronous calls](/docs/developers/sync-vs-async).

For text longer than 500 characters, split at sentence boundaries and send
the parts as separate asynchronous jobs. Each one comes back with its own
`audioUrl`, and you join the files on your side.

## Let the result come to you

An asynchronous job moves through `pending` and `processing` and ends as
`completed` or `failed`. You can learn about the end in two ways:

| Way                                       | Delay after the job ends                                                                                                               | Requests it costs                             |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| A `job.completed` or `job.failed` webhook | Seconds. The delivery is sent when the job ends, with a 10-second timeout per attempt, and retried after 5 and 10 seconds if it fails. | None against your key.                        |
| Polling `GET .../jobs/{jobId}`            | Up to one polling interval.                                                                                                            | One per poll, counted against the rate limit. |

The webhook body carries the result itself: `audioUrl` for Text to Speech,
`transcription` for Speech to Text, and `durationSeconds` for both. You do
not need to call the job endpoint after the event arrives. Register an
endpoint on [Configure a webhook](/docs/developers/webhooks-configure) and
verify each delivery as on
[Verify a webhook signature](/docs/developers/webhooks-verify-signature).

**Recommended**

- A webhook on `job.completed` and `job.failed`, plus one poll of the job if
  no event has arrived 15 minutes after the `POST`.

**Less effective**

- A poll loop per job, running from the moment the `POST` returns until the
  status changes.

If you must poll, do the arithmetic against the rate limit first. A key may
make 100 requests per 60 seconds on each endpoint, and the job endpoint is
one endpoint whatever the `jobId`. Poll at a fixed interval of 3 seconds
for Text to Speech and 5 seconds for Speech to Text, never under 1 second;
the full pattern is on
[Retry requests safely](/docs/developers/retries-and-idempotency). At 3
seconds one job costs 20 requests per minute, so five jobs polled at once
on one key fill the limit; at 5 seconds it is 12 per minute and eight jobs.
Beyond that, lengthen the interval, spread the jobs over several keys, or
switch to webhooks. Stop polling as soon as `status` is `completed` or
`failed`; polling faster does not finish the job sooner.

## Poll the job when no event comes

A job that stays in `processing` for more than 10 minutes is marked
`failed`, with `error` set to `Job timed out — AI server did not respond`;
the status changes within 15 minutes of the `POST`. This timeout sends no
`job.failed` webhook, so a client that only listens for events never hears
about it. Set your own deadline: if neither event has arrived 15 minutes
after the `POST`, fetch the job once and read its `status`.

## Reuse the connection

Every request to the API is over HTTPS, and a new connection pays for a TCP
handshake and a TLS handshake before the first byte of your request is sent.
Keep one HTTP client per process and let it reuse connections. In Python,
create one `requests.Session()` and call it for every request. In Node, the
built-in `fetch` keeps connections alive by default; do not create a new
agent per call. A tight loop that polls with a fresh connection each time
spends more time in handshakes than in the API.

## Cache the voice list

`GET /v1/text-to-speech/voices` returns every active voice in one response,
with no paging and no filters, so there is nothing to gain from calling it
per request. Fetch it when your process starts, keep it in memory, and
filter on `language` locally. Refresh it when a generation call answers
`404` with `Voice not found`, which is what the API says when a voice has
been deactivated since you cached the list. The fields are on
[Text to Speech voices and options](/docs/developers/tts-voices-and-options).

## Send audio the API can measure

The two upload endpoints read the file's duration from its metadata before
any work runs, both to apply the 10-minute cap and to price the
call. A file whose duration cannot be read is not rejected: it is accepted
and priced from its size instead, at one second per 16 000 bytes, rounded
up. Whether that is more or less than the real length depends on the file's
bitrate, and the charge is not corrected afterwards. Write files with a
proper header so the charge matches the audio, and prefer a compressed
container: a smaller file spends less time in transfer.

## Download the audio once

`audioUrl` is a signed link to the audio file, and the link expires.
Download the file when you receive the link,
store it on your side, and serve it from there. Do not embed the link in a
page or a message that will be opened later. If a link has expired,
`GET /v1/text-to-speech/jobs/{jobId}` returns a fresh one on every call.

## Give each system its own key

The rate limit is counted per API key on each endpoint, not per workspace.
One busy batch job on a shared key can push an interactive service on the
same key into `429`. Create one key per system that calls the API, so that
each has its own 100 requests per 60 seconds, and so that a limit on one key
does not delay the others. Your plan sets how many active keys a workspace
may have; see [Get an API key](/docs/developers/get-an-api-key).

## FAQ

### Is the asynchronous endpoint faster?

No. The work takes the same time either way. The asynchronous endpoint
frees your connection and lets you run many jobs at once; the synchronous
endpoint gives you the result in one round trip.

### Does polling more often finish the job sooner?

No. It only spends your 100 requests per minute faster. The job ends when
the work ends.

### How long can I wait for a synchronous call?

There is no server-side cap on a synchronous call. Set your client timeout
from the input size: a 10-minute file takes longer than a 10-second one.

### Is the 15-minute deadline a guarantee?

Yes. A job that stays `processing` for more than 10 minutes is marked
`failed` within 15 minutes of the `POST`, so a job is never `processing`
for longer than that.

## Related

- [Synchronous and asynchronous calls](/docs/developers/sync-vs-async)
- [Retry requests safely](/docs/developers/retries-and-idempotency)
- [Rate limits](/docs/developers/rate-limits)
- [Configure a webhook](/docs/developers/webhooks-configure)
- [Webhook events](/docs/developers/webhooks-events)
- [Delivery, retries and failures](/docs/developers/webhooks-retries)
- [Text to Speech quickstart](/docs/developers/tts-quickstart)
- [Speech to Text quickstart](/docs/developers/stt-quickstart)
- [Errors](/docs/developers/errors)
- [Security best practices](/docs/developers/best-practices-security)
