Every Comprehend capability comes in two calling shapes. A synchronous Detect* call takes one string and returns a result in under a second. An asynchronous Start*Job call takes an S3 prefix full of documents, processes all of them, and writes results to another S3 prefix - taking minutes, not milliseconds.
Neither shape is "better" in general - they trade off latency, cost, and IAM setup differently, and picking wrong shows up as either an unnecessarily slow loop of synchronous calls or an over-engineered batch pipeline for something that needed one quick answer. This page lays out the decision and the job lifecycle.
Every Start*Job operation - StartEntitiesDetectionJob, StartKeyPhrasesDetectionJob, StartSentimentDetectionJob, StartPiiEntitiesDetectionJob, StartDominantLanguageDetectionJob, and the custom-model jobs StartDocumentClassificationJob/StartEntitiesDetectionJob with a recognizer ARN - takes the same three core inputs: an InputDataConfig (S3 location plus InputFormat, either ONE_DOC_PER_FILE or ONE_DOC_PER_LINE), an OutputDataConfig (S3 location for results), and a DataAccessRoleArn that Comprehend assumes to read the input and write the output. None of that IAM setup exists for a synchronous Detect* call, which needs only the calling identity's own comprehend:Detect* permission.
A synchronous call is Comprehend acting on data you send it directly in the request. A batch job is Comprehend reaching into your S3 buckets on its own, asynchronously, without you present - so it needs its own permission to do that, scoped by a role you create and trust it to assume. This is the most common setup failure for a first batch job: the role exists, but its trust policy doesn't allow the Comprehend service principal to assume it, or its permissions policy doesn't cover the specific input/output prefixes.
There is no webhook or event that fires when a Comprehend job finishes. You poll Describe*Job (matching the job type you started) on an interval and read JobStatus. Terminal states are COMPLETED, FAILED, and STOPPED (if you called Stop*Job); anything else - SUBMITTED, IN_PROGRESS - means keep waiting. For production pipelines, back this polling loop with a reasonable interval (seconds to a minute, not a tight loop) and a timeout so a stuck job doesn't hang your process indefinitely.
Synchronous calls are billed per unit of text per call and return in under a second - ideal for anything a user or another system is actively waiting on. Batch jobs are billed similarly per unit of text processed, but the value is in throughput: one job call, one IAM setup, and Comprehend handles distributing work across your documents, rather than your code looping over thousands of synchronous calls itself. For custom models specifically, batch has an additional advantage: it calls the trained model ARN directly, with no hosted endpoint and therefore no hourly cost between runs - a real-time endpoint bills continuously whether or not it receives traffic.
A borderline case is a moderate number of documents (dozens to a few hundred) needed fairly soon but not instantly. Looping synchronous calls works but adds request overhead and client-side error handling for each one; a batch job adds job-management code (start, poll, read output) but only one call to kick it off. As a rule of thumb, reach for batch once you're processing enough documents that a client-side loop starts to feel like reimplementing what Start*Job already does for you.
Expecting Start*Job to return results synchronously - it only returns a JobId; you must poll for completion. Fix: implement a Describe*Job polling loop with a sane interval and timeout.
Missing or misconfigured DataAccessRoleArn trust policy - the job fails immediately if Comprehend can't assume the role. Fix: verify the role's trust policy allows the comprehend.amazonaws.com service principal, and its permissions cover the exact input/output S3 prefixes.
Looping synchronous Detect calls over thousands of documents - this is slow, expensive in request overhead, and reimplements what a batch job already does. Fix: switch to the matching Start*Job once document counts grow past a client-side loop's comfort zone.
Leaving a custom model's real-time endpoint running between infrequent batch-like workloads - it bills hourly regardless of traffic. Fix: use StartDocumentClassificationJob/StartEntitiesDetectionJob against the model ARN instead of standing up an endpoint for bursty or infrequent work.
Polling too aggressively - hammering Describe*Job in a tight loop wastes calls without speeding up the job. Fix: poll on an interval of seconds to a minute, matched to how quickly you actually need to know.
Not handling FAILED or STOPPED as distinct from COMPLETED - some pipelines only check for success and hang or silently drop failures. Fix: branch explicitly on all terminal states and surface failures.
Does every Detect operation have a batch equivalent?
Yes - entities, key phrases, sentiment, PII, and dominant language each have a matching Start*Job operation that processes an S3 prefix instead of one string.
How do I know when a batch job is done?
Poll Describe*Job (matching the job type) with the JobId and read JobStatus. It moves through SUBMITTED and IN_PROGRESS to a terminal state: COMPLETED, FAILED, or STOPPED.
Why do batch jobs need an IAM role that synchronous calls don't?
A batch job has Comprehend reading and writing your S3 buckets asynchronously on your behalf, so it needs a role it can assume with permission to do that. A synchronous call only needs your own calling identity's permission.
Is batch cheaper than real-time?
Per unit of text, pricing is similar. The savings come from avoiding a continuously billed real-time endpoint for custom models, and from avoiding the overhead of looping many synchronous calls yourself.
What InputFormat options does a batch job support?
ONE_DOC_PER_FILE (each file in the S3 prefix is one document) or ONE_DOC_PER_LINE (each line within files is a separate document) - choose based on how your source data is structured.
Can I stop a batch job partway through?
Yes, with the matching Stop*Job operation (for example StopSentimentDetectionJob). The job's JobStatus then reports STOPPED rather than COMPLETED.
When should I default to batch over real-time?
Whenever nothing is actively waiting on the result and you can tolerate minutes of latency - it's typically the cheaper, simpler default for archives, nightly processing, and infrequent custom-model inference.