Skip to main content

Speech recognition best practices

Prepare audio, reuse connections, manage Java concurrency and asynchronous jobs, and handle errors in speech recognition applications.

Prepare input audio

Check the input format, sample rate, channels, and duration against the target model’s audio specifications.
  • Prioritize capture quality and reduce background noise and echo. Before enabling noise suppression or AEC, compare recognition results before and after processing on the same representative, manually transcribed audio set.
  • For 8 kHz telephone audio, choose a matching model and sample rate.
  • Choose a model that supports the audio’s language and dialect.
  • For inaccurate recognition of specialized terms, configure hotwords or context enhancement as described in Improve recognition accuracy.
  • Inspect the audio format
  • Convert the encoding and sample rate
  • Trim audio
  • Extract audio from video
Use FFprobe to inspect the container format, codec, sample rate, and channels:
ffprobe -v error -show_entries format=format_name -show_entries stream=codec_name,sample_rate,channels -of default=noprint_wrappers=1 input.mp3

Choose a file input method

File input methods depend on the API. For example, Qwen-Audio-ASR-Flash supports audio file URLs and Base64 Data URIs. Encode local audio as a Base64 Data URI to submit it directly. For encoding and invocation examples, see Flash HTTP API. When submitting a file by URL, ensure the recognition service can download it while processing the task. Check permissions, URL expiration, and public network accessibility. For production, host files in stable storage such as OSS and submit their URLs. Reuse uploaded files during batch processing to reduce repeated uploads. Before using an SDK's local file upload feature, check the upload limits of that interface. See Get a temporary file URL for the temporary file mechanism and its conditions.

Manage real-time tasks and connections

The run-task / finish-task protocol used by Qwen-Audio-ASR-Message, Qwen-Audio-ASR-Streaming, Fun-ASR-Realtime, and Paraformer supports sequential connection reuse:
  1. Send run-task with a unique task_id. Wait for task-started before sending audio.
  2. After sending the audio, send finish-task with the same task_id.
  3. Wait for the server's task-finished event to confirm completion.
  4. Start the next task on the same connection with a new task_id. Do not run tasks concurrently on one connection.
Qwen3-ASR-Realtime uses a session protocol. Close the connection after the session ends. For connection and termination procedures, see Qwen3-ASR-Realtime interaction flow.

Handle idle and failed connections

  • The connection closes automatically if no new task starts for approximately 60 seconds after task completion. Establish a new connection after an extended idle period.
  • After task failure, network interruption, or a protocol error, close the old connection and create a new recognition object. Invalidate pooled objects instead of returning them as usable objects.
  • Close the object pool and its connections when the application exits or stops providing recognition.

Handle extended silence during a task

If a running task needs to receive continuous silent audio, configure heartbeat=true as described by its API and continue sending silence in the required encoding. Manage idle connections after task completion as described in the preceding section. For model parameters, see the following references. Message client events | Streaming client events | Fun-ASR client events | Paraformer client events

Configure Java concurrency

Give each running recognition task exclusive use of a Recognition object and return it only after successful completion. The following example manages recognition objects with Commons Pool 2. Create the pool once and reuse it across calls.

Add dependencies

Complete Get an API key and Install an SDK first. The complete example in this section requires DashScope Java SDK 2.18.0 or later. These dependency configurations use DashScope Java SDK 2.23.2 and Commons Pool 2.12.0:
  • Maven
  • Gradle
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dashscope-sdk-java</artifactId>
    <version>2.23.2</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.12.0</version>
</dependency>

Distinguish connection, object, and request capacity

The SDK reads the following three DASHSCOPE_ environment variables. Set them before starting the process. The example in this document reads RECOGNITION_OBJECTPOOL_SIZE.
Environment variableDefaultPurpose
DASHSCOPE_CONNECTION_POOL_SIZE32Idle HTTP connections retained by the SDK's shared OkHttp client.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS32Concurrent scheduling capacity for SDK asynchronous requests. Requests above this capacity wait for scheduling.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST32Asynchronous request scheduling capacity per host.
RECOGNITION_OBJECTPOOL_SIZE500Total recognition objects allowed by this example, controlled by Commons Pool's maxTotal.
Concurrency is the number of tasks running at the same time; RPS is the number of requests per second. Configure worker threads and object pool capacity for application concurrency, and control task start frequency according to the model’s Rate limits.
The following values are capacity planning references for Paraformer real-time recognition applications. Per-server concurrency means recognition tasks running at the same time. The server runs only that recognition application.
  • For HTTP idle connection capacity, start from at least twice peak concurrency and adjust as needed.
  • Initially set total and per-host asynchronous scheduling capacity to the same value as that connection capacity, then adjust based on observed queueing.
  • For recognition object pool capacity, start from 1.5 to 2 times peak concurrency. The example default of 500 is an application setting.
Alibaba Cloud serverReference task concurrencyRecognition pool capacityHTTP idle connection capacity
4 vCPUs, 8 GiB1005002000
8 vCPUs, 16 GiB2005002000
16 vCPUs, 32 GiB4005002000
These settings apply to a server dedicated to the Paraformer real-time recognition application. Adjust capacity for audio transmission rate, client resources, and application workload. For model API limits, see Rate limits.

Implement a recognition object pool

Use the complete lifecycle below. When maxTotal is reached and no idle object is available, this example’s borrowObject waits for an object to be returned. Objects are created on demand, and WebSocket connections are established when recognition requests start.
  1. Borrow a Recognition object, start recognition, and send audio.
  2. Send the end command and wait for completion. For asynchronous calls, also check the error callback.
  3. After success, call returnObject so subsequent tasks can reuse the object.
  4. On failure or interruption, call invalidateObject. The factory's destroyObject closes the connection.
  5. After all tasks finish, close the pool to release connections held by idle objects.
Save the code as RecognitionPoolExample.java. Prepare asr-example.pcm containing 16 kHz, 16-bit, mono PCM audio, set DASHSCOPE_API_KEY, and replace the workspace and endpoint with the configuration for the target region (see Endpoints). The example uses three worker threads to process nine recognition tasks.
import com.alibaba.dashscope.audio.asr.recognition.Recognition;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionParam;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionResult;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.utils.Constants;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

public class RecognitionPoolExample {
    private static final int WORKERS = 3;
    private static final int TASKS = 9;
    private static final int COMPLETION_TIMEOUT_SECONDS = 60;

    public static void main(String[] args) {
        int exitCode = 0;
        try {
            run(args);
        } catch (Exception e) {
            e.printStackTrace();
            exitCode = 1;
        }
        // Exit this standalone example after all owned resources are closed.
        System.exit(exitCode);
    }

    private static void run(String[] args) throws Exception {
        String apiKey = System.getenv("DASHSCOPE_API_KEY");
        if (apiKey == null || apiKey.isEmpty()) {
            throw new IllegalArgumentException("Set DASHSCOPE_API_KEY first");
        }
        // Replace {WorkspaceId} with the workspace ID for the endpoint region.
        Constants.baseWebsocketApiUrl =
                "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference";
        Path audio = Paths.get(args.length == 0 ? "asr-example.pcm" : args[0]);
        if (!Files.isReadable(audio)) {
            throw new IllegalArgumentException("Audio file is not readable: " + audio);
        }
        int capacity = Integer.parseInt(System.getenv().getOrDefault(
                "RECOGNITION_OBJECTPOOL_SIZE", "500"));
        if (capacity <= 0) {
            throw new IllegalArgumentException("Pool size must be positive");
        }
        GenericObjectPoolConfig<Recognition> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(capacity);
        config.setMaxIdle(capacity);
        config.setBlockWhenExhausted(true);
        config.setMinIdle(0);

        GenericObjectPool<Recognition> pool =
                new GenericObjectPool<>(new RecognitionFactory(), config);
        ExecutorService executor = Executors.newFixedThreadPool(WORKERS);
        ExecutorService completionExecutor = Executors.newFixedThreadPool(WORKERS);
        try {
            List<Callable<Void>> tasks = new ArrayList<>();
            for (int i = 0; i < TASKS; i++) {
                tasks.add(() -> {
                    recognize(pool, audio, apiKey, completionExecutor);
                    return null;
                });
            }
            // Wait for every task, then propagate any task failure.
            List<Future<Void>> results = executor.invokeAll(tasks);
            for (Future<Void> result : results) {
                result.get();
            }
        } finally {
            executor.shutdown();
            completionExecutor.shutdownNow();
            try {
                if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
                    executor.shutdownNow();
                }
                if (!completionExecutor.awaitTermination(1, TimeUnit.MINUTES)) {
                    completionExecutor.shutdownNow();
                }
            } finally {
                pool.close();
                System.out.printf("Pool closed: created=%d, destroyed=%d, active=%d, idle=%d%n",
                        pool.getCreatedCount(), pool.getDestroyedCount(),
                        pool.getNumActive(), pool.getNumIdle());
            }
        }
    }

    private static void recognize(GenericObjectPool<Recognition> pool,
                                  Path audio, String apiKey,
                                  ExecutorService completionExecutor) throws Exception {
        // Example timeout; adjust it to the application's queueing policy.
        Recognition recognizer = pool.borrowObject(Duration.ofSeconds(30));
        boolean reusable = false;
        AtomicReference<Exception> failure = new AtomicReference<>();
        CountDownLatch completed = new CountDownLatch(1);
        try (InputStream input = Files.newInputStream(audio)) {
            RecognitionParam param = RecognitionParam.builder()
                    .model("qwen-audio-3.1-asr-flash-streaming")
                    .apiKey(apiKey).format("pcm").sampleRate(16000).build();
            ResultCallback<RecognitionResult> callback = new ResultCallback<RecognitionResult>() {
                @Override
                public void onEvent(RecognitionResult result) {
                    if (result.isSentenceEnd() && result.getSentence() != null) {
                        System.out.println(result.getSentence().getText());
                    }
                }

                @Override
                public void onComplete() {
                    completed.countDown();
                }

                @Override
                public void onError(Exception error) {
                    failure.set(error);
                    completed.countDown();
                }
            };
            recognizer.call(param, callback);
            // 100 ms of PCM16 mono audio at 16 kHz is 3,200 bytes.
            byte[] buffer = new byte[3200];
            int count;
            while ((count = input.read(buffer)) != -1) {
                if (failure.get() != null) {
                    throw failure.get();
                }
                recognizer.sendAudioFrame(ByteBuffer.wrap(buffer, 0, count));
                Thread.sleep(100);
                buffer = new byte[3200];
            }
            // stop() blocks in the SDK, so apply the deadline to the stop call itself.
            long deadline = System.nanoTime()
                    + TimeUnit.SECONDS.toNanos(COMPLETION_TIMEOUT_SECONDS);
            Future<?> stopTask = completionExecutor.submit(recognizer::stop);
            try {
                stopTask.get(Math.max(0, deadline - System.nanoTime()), TimeUnit.NANOSECONDS);
                long remaining = Math.max(0, deadline - System.nanoTime());
                if (!completed.await(remaining, TimeUnit.NANOSECONDS)) {
                    throw new TimeoutException("Timed out waiting for task completion");
                }
            } finally {
                stopTask.cancel(true);
            }
            if (failure.get() != null) {
                throw failure.get();
            }
            reusable = true;
            System.out.printf("Completed: request_id=%s, first_packet_ms=%d, last_packet_ms=%d%n",
                    recognizer.getLastRequestId(), recognizer.getFirstPackageDelay(),
                    recognizer.getLastPackageDelay());
        } finally {
            if (reusable) {
                pool.returnObject(recognizer);
            } else {
                // destroyObject closes the connection before discarding the object.
                pool.invalidateObject(recognizer);
            }
        }
    }

    private static class RecognitionFactory extends BasePooledObjectFactory<Recognition> {
        @Override
        public Recognition create() {
            return new Recognition();
        }

        @Override
        public PooledObject<Recognition> wrap(Recognition recognizer) {
            return new DefaultPooledObject<>(recognizer);
        }

        @Override
        public void destroyObject(PooledObject<Recognition> object) {
            if (object.getObject().getDuplexApi() != null) {
                object.getObject().getDuplexApi().close(1000, "pool cleanup");
            }
        }
    }
}
The example sets a 30-second object borrow timeout and a 60-second task completion timeout. Adjust both as needed. For live capture, set the wait limit according to the permitted startup latency and bound the number of queued tasks. When changing the audio source, retain completion confirmation, failure invalidation, and resource cleanup.

Warm up and measure latency

Warm up by sending actual recognition requests to establish WebSocket connections that subsequent tasks can reuse.
  1. Send requests at the intended production concurrency before collecting measurements. One to two minutes is a starting reference; adjust it to the workload.
  2. Confirm that reusable connections exist before collecting steady-state latency. Report cold-start and steady-state data separately.
  3. Connections can close during application idle periods, requiring setup on a later call. Warm up again as needed before new performance measurements.
Record the SDK's first-packet and last-packet latency alongside application end-to-end latency, with explicit start and end points for each metric. Queueing, connection setup, audio transmission, and service processing affect different metrics and should be observed separately.

Manage asynchronous transcription jobs

For high-concurrency batch transcription, prefer task completion callbacks to reduce polling requests. For callback configuration, message structure, and result retrieval, see Configure asynchronous task callbacks.

Control submission, query, and upload rates

  • Task submission: Control submission frequency according to the target model’s Rate limits.
  • Status queries: The query API has a default limit of 20 QPS, which can be increased to 100 QPS (contact your Alibaba Cloud account manager to request an increase), per Alibaba Cloud primary account, including its RAM users. When polling, start with an interval of 2 to 5 seconds and adjust it to the number of tasks so that the total query rate remains within the limit. For queries, batch queries, and task cancellation, see Manage asynchronous tasks.
  • Temporary file uploads: The file upload credential API is limited to 100 QPS per Alibaba Cloud primary account and model. This limit cannot be increased. Use stable storage such as OSS for production and high-concurrency workloads. For temporary upload procedures and limits, see Get a temporary file URL.

Save recognition results promptly

Download and save result files promptly after task success. For example, transcription_url in Qwen-Audio-ASR-Filetrans HTTP API is valid for 24 hours. Download and persist the results before it expires.
  • When a response contains a results array, check each subtask_status before downloading transcription JSON for successful files. Save the timestamps, speaker information, and other fields needed by the application.
  • Retain the task-to-file mapping and final status. If a download fails, keep the task record and retry within the URL validity period.

Handle errors and retries

Decide whether to retry based on the error type. Record the request ID, task ID, model, region, and failure stage to distinguish input and permission issues from temporary service failures.
IssueAction
Input file or parameter errorsCheck the URL, encoding, sample rate, channels, and parameter ranges. Correct the issue before submitting again.
Authentication or endpoint configuration errorsCheck the API key, workspace, and endpoint region, then correct the configuration according to the error message.
Rate limitingReduce request frequency or task start rate and follow Rate limits. Avoid immediate retry loops.
Network timeouts, interruptions, or temporary server errors (5xx)For failed queries or downloads, use backoff retries with limits on attempts and total wait time. After an asynchronous submission timeout, query the task if a task_id was received. If no task_id was received, decide whether to resubmit based on the application’s tolerance for duplicate processing and charges.
Asynchronous task or file processing failuresRetain task-level and file-level errors, identify the cause, then decide whether to correct the input or resubmit.
Use exponential backoff with jitter and cap both retry attempts and total wait time. For example, allow at most three retries and adjust the limits to the application’s latency budget. After a real-time disconnection, close the old connection and start a task with a new ID. Track audio position using the last confirmed final result. Replay unconfirmed audio from the application buffer as needed, and merge and deduplicate results using positions in the original audio.