When your localization pipeline needs to process hundreds of thousands of strings, product catalogs, legal disclosures, help-center articles, UI copy across dozens of locales, synchronous, one-request-at-a-time API calls collapse under their own weight. Rate limits throttle you, timeouts kill long-running jobs, and partial failures leave your database in an inconsistent state with no clear path to recovery. The real engineering challenge isn't calling a translation endpoint; it's building a reliable, high-throughput batch pipeline that can submit work asynchronously, listen for completions via webhooks, handle every failure mode gracefully, and do it all without translating the same sentence twice. This article is the blueprint for that system.
Ollang is the AI execution layer for enterprise localization, integrating async batching, translation memory, and quality controls across text, audio, video, software, websites, and legal documents. If you're evaluating how to wire async translation into your stack, schedule a walkthrough with Ollang's integration team to see how these patterns map to your architecture.
Why Synchronous Translation Calls Break at Scale
Synchronous translation API calls follow a simple pattern: send text, wait for the response, move on. That works for a settings page with twelve strings. It fails when you need to localize 200,000 product descriptions into 14 languages overnight.
The first wall you hit is rate limits. Most translation APIs enforce per-second or per-minute request caps. If you're limited to 100 requests per second and each request carries a single paragraph, translating a million paragraphs takes hours of uninterrupted, perfectly paced calls, assuming zero errors.
The second wall is timeout risk. Synchronous calls that carry large payloads or target multiple languages in one request can exceed gateway timeouts (commonly 30-60 seconds), causing the entire request to fail even if the translation engine was still working.
The third wall is resource waste. Your application thread or worker sits idle while waiting for a response. At scale, you're burning compute on waiting, not working.
Latency Ceilings and Timeout Risks
Even well-optimized translation engines need time to process large segments. A single synchronous request carrying a 50 KB JSON payload with nested HTML markup can take several seconds to parse, segment, translate, and reassemble. Multiply that by thousands of concurrent users or batch jobs, and you're stacking latency on top of latency. Gateway timeouts (HTTP 504) become routine rather than exceptional, and each one represents work that may or may not have completed on the server side, leaving you with an ambiguous state that's expensive to reconcile.
Thread-Pool Exhaustion in Monolithic Pipelines
In a monolithic pipeline where a single process iterates through a queue of translation requests synchronously, thread-pool exhaustion is inevitable. Each blocked thread waiting on an HTTP response is a thread unavailable for other work. In Java or .NET services with fixed thread pools, this leads to request queuing, cascading timeouts, and eventually service-level failures that affect far more than localization. The async pattern exists precisely to decouple submission from completion, freeing your application to do useful work while the translation engine processes in the background.
Designing an Async Batch Translation Architecture
An async batch architecture separates three concerns: job submission, job processing, and result delivery. Your system submits work, receives an acknowledgment with a job identifier, and then learns about completion through either polling or a webhook callback. This decoupling is what makes scale possible.
Job Submission Endpoints and Payload Design
A well-designed job submission endpoint accepts a structured payload containing the source content, source language, target language(s), and any processing directives (glossary IDs, formality settings, translation-memory references). The response is immediate: an HTTP 202 Accepted with a job ID and an estimated completion time.
Here's a representative submission payload:
{
"source_language": "en",
"target_languages": ["de", "fr", "ja", "pt-BR"],
"content": [
{
"key": "product.description.12847",
"text": "Lightweight running shoe with responsive cushioning.",
"context": "product catalog"
},
{
"key": "product.description.12848",
"text": "Waterproof hiking boot rated for sub-zero temperatures.",
"context": "product catalog"
}
],
"callback_url": "https://api.yourcompany.com/webhooks/translation",
"glossary_id": "gl_sportswear_2024",
"idempotency_key": "batch-20250115-catalog-run3"
}
The idempotency_key is critical. If your submission request times out and you retry, the API should recognize the duplicate and return the existing job ID rather than creating a second job. Without idempotency, retries create duplicate work and duplicate charges.
Polling vs. Webhook Callbacks
You have two options for learning when a job completes: polling and webhooks.
- Polling means your system periodically calls a status endpoint (e.g., GET /jobs/{job_id}) to check whether the job is done. It's simple to implement but wasteful, most polls return "still processing", and it introduces a latency floor equal to your polling interval.
- Webhooks mean the translation service calls your endpoint when the job completes (or fails). This is event-driven, efficient, and gives you near-instant notification. The tradeoff is that you must expose a publicly reachable HTTPS endpoint, validate incoming signatures, and handle the possibility that the webhook delivery itself fails.
The production-grade approach is to use webhooks as the primary notification channel with polling as a fallback. If you haven't received a webhook within a reasonable window (say, 2Ă— the estimated completion time), your system polls to reconcile.
# Simplified webhook handler (Flask)
from flask import Flask, request, jsonify
import hmac, hashlib
app = Flask(__name__)
WEBHOOK_SECRET = b"your_shared_secret"
@app.route("/webhooks/translation", methods=["POST"])
def handle_translation_webhook():
signature = request.headers.get("X-Signature-256")
expected = hmac.new(WEBHOOK_SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({"error": "invalid signature"}), 401
payload = request.json
job_id = payload["job_id"]
status = payload["status"]
if status == "completed":
store_translations(job_id, payload["results"])
elif status == "partial_success":
store_translations(job_id, payload["results"])
enqueue_retry(job_id, payload["failed_segments"])
elif status == "failed":
send_alert(job_id, payload["error"])
return jsonify({"received": True}), 200
Need help choosing and validating your webhook strategy? Discuss your webhook design with Ollang.
File and Document Format Support
Enterprise localization rarely involves plain strings alone. You need to translate DOCX contracts, HTML help articles, JSON resource bundles, XLIFF files, and sometimes PDF or subtitle formats. A capable async API accepts these as file uploads, handles internal segmentation (splitting a DOCX into translatable text runs while preserving formatting), and returns the translated file in the same format.
Key evaluation criteria across providers:
- Supported formats: DOCX, XLSX, HTML, JSON, XLIFF 1.2/2.0, SRT, PO, ARB, XML
- Max file size: varies widely; some cap at tens of MB, others allow 100 MB+
- Segmentation: does the API segment internally, or must you pre-segment?
- Markup preservation: are HTML tags, placeholders like {username}, and ICU patterns preserved?
- Multi-file jobs: can you submit a ZIP or batch of files in a single job?
When the API handles segmentation internally, you avoid the fragile work of parsing DOCX XML or splitting HTML without breaking tag nesting. When it doesn't, you need a pre-processing layer, and that layer becomes a source of bugs.
Ollang's platform handles internal segmentation and returns translated files in their original formats, reducing the need for brittle pre-processing in many enterprise workflows.
Chunking Strategies for Large Payloads
Even with async endpoints, you can't submit a 500 MB JSON file as a single request. You need to chunk intelligently.
Segment-Level vs. Document-Level Batching
- Document-level batching means each API job corresponds to one document (one DOCX file, one HTML page). This preserves context for the translation engine, which can use surrounding sentences to disambiguate meaning. It's the right default when documents are reasonably sized.
- Segment-level batching means you pre-segment content into individual strings or paragraphs and group them into batches of a fixed size (e.g., 500 segments per request). This gives you fine-grained control over payload size and makes partial-failure recovery simpler, you know exactly which segments failed. The tradeoff is loss of document-level context.
A practical hybrid approach: batch at the document level when documents are small, and switch to segment-level batching for large documents or string-table workloads. Use a threshold, say, 5,000 segments or 1 MB of text, to decide.
Respecting Maximum Payload Sizes and Segmentation Limits
Every API has limits, even if they're not always well-documented. Common constraints include:
- Maximum request body size (JSON): often 10-50 MB
- Maximum segments per request: sometimes capped at 1,000-5,000
- Maximum characters per segment: commonly 5,000-10,000
- Maximum target languages per request: some require one request per target language
Build your chunking logic to respect all of these simultaneously. A chunk that fits under the byte limit might still exceed the segment count limit. Validate both before submission.
def chunk_segments(segments, max_segments=500, max_bytes=5_000_000):
chunk = []
chunk_bytes = 0
for seg in segments:
seg_bytes = len(seg["text"].encode("utf-8"))
if (len(chunk) >= max_segments) or (chunk_bytes + seg_bytes > max_bytes):
yield chunk
chunk = []
chunk_bytes = 0
chunk.append(seg)
chunk_bytes += seg_bytes
if chunk:
yield chunk
Rate-Limit Compliance and Throughput Planning
Hitting rate limits isn't a sign that you're doing something wrong, it's a sign that you need a smarter submission strategy. Translation APIs typically enforce rate limits using a token-bucket algorithm, where you're allocated a certain number of tokens (requests or characters) per time window, and each request consumes tokens.
Token-Bucket Patterns and Adaptive Throttling
A token-bucket rate limiter refills at a steady rate. If you burst above the refill rate, you'll drain the bucket and start receiving HTTP 429 (Too Many Requests) responses. The response headers often tell you how long to wait:
HTTP/1.1 429 Too Many Requests
Retry-After: 2
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705334400
Your client should read these headers and back off accordingly. A simple adaptive throttle works like this:
1. Start at your known rate limit (e.g., 50 requests/second).
2. On a 429, pause for the Retry-After duration.
3. After resuming, reduce your submission rate by 20%.
4. Gradually increase back toward the limit as successful responses accumulate.
This prevents the "thundering herd" problem where multiple workers all resume simultaneously after a backoff window and immediately trigger another round of 429s.
Forecasting Throughput and Cost per Million Characters
Before launching a batch pipeline in production, model your throughput and cost:
- Throughput: multiply your safe requests/second by average characters/request; then discount by 20-30% for retries, backoff, and variance.
- Cost: translation API pricing is typically per character or per word. At scale, even small per-character costs compound across many languages. Translation-memory deduplication (covered below) is the single most effective lever for reducing this cost.
Build a cost model before you scale. Include not just API fees but also compute for submission workers, webhook infrastructure, and storage for translated assets.
