Building automated text analysis pipelines on top of social media platforms is notoriously fragile. When dealing with short-form video, developers face a compounding problem. Not only do they have to deal with the volatile nature of video delivery networks, but they also have to parse subtitle formats, reconcile machine-translated variants, and handle videos that contain no speech whatsoever.
To solve this, developers often turn to the TikTok Transcript Scraper to pull subtitle tracks and plain-text transcripts from TikTok videos without needing to manage cookies, browser sessions, or user logins. However, integrating this tool into a robust data pipeline requires understanding several structural realities. The underlying API schemas, platform limits, and execution models present distinct failure modes that are implied by the documentation but never explicitly spelled out.
This article analyzes these failure modes, details how the Apify platform constraints interact with your execution scripts, and provides defensive code implementations to keep your pipeline from crashing.
Checked against the Actor's input schema and Apify docs on 2026-09-19.
What happens when a TikTok video has no subtitles?
When a video has no subtitles, the Actor does not throw an API error or exit with a non-zero system code; instead, it writes a single record to the dataset containing a sentinel failure payload. This output item includes the video metadata but sets a success flag to false accompanied by an error message. Your pipeline must intercept this specific shape before parsing transcripts or looping over timing segments.
If your downstream consumer expects every returned row to contain a valid transcript string and a populated segments array, a video without native subtitles will crash your ingest workers. In a typical execution run, popular educational or news videos might emit multiple rows (one for each available language), but a music-only slideshow or a silent video will emit exactly one row that looks like this:
{
"postId": "7123456789012345678",
"postUrl": "https://www.tiktok.com/@somecreator/video/7123456789012345678",
"username": "somecreator",
"displayName": "Some Creator",
"caption": "My silent slideshow post",
"createdAt": "2026-09-19T12:00:00Z",
"duration": 15,
"success": false,
"message": "No captions available for this video"
}
Notice that empty fields like transcript, segments, and languageCode are omitted entirely from the JSON payload. A naive parser trying to access row.transcript.toLowerCase() or iterate over row.segments without checking the success key will immediately throw a TypeError.
Here is how to defensively process these rows in a Node.js pipeline:
function processDatasetRow(row) {
if (row.success === false) {
console.warn(`Skipping video ${row.postId}: ${row.message || 'No captions available'}`);
return null;
}
if (!row.transcript || !Array.isArray(row.segments)) {
console.warn(`Row for ${row.postId} (${row.languageCode}) is missing expected transcript fields.`);
return null;
}
return {
id: row.postId,
language: row.languageCode,
text: row.transcript,
cues: row.segments.map(seg => ({
start: seg.start,
end: seg.end,
text: seg.text
}))
};
}
Why does the Apify synchronous run endpoint time out past 300 seconds?
The synchronous run endpoint on the Apify platform hard-caps at 300 seconds and returns an HTTP 408 status code if the execution takes longer. This is a critical barrier when using the Whisper fallback option because transcribing audio files locally requires intensive processing that easily exceeds five minutes. To avoid this timeout, you must run the Actor asynchronously, poll the run status, and fetch the dataset when complete.
For fast execution pipelines, you might be tempted to use synchronous calls to trigger the run and wait for the results in a single round-trip. This works fine for small batches of standard WebVTT tracks. However, if you pass a long list of postUrls or enable Whisper audio processing, the run will almost certainly hit the 5-minute wall.
To prevent this failure, you must split your logic. POST an asynchronous request to trigger the run, poll the run status, and fetch the dataset items only when the run is complete.
Here is a Python script using the requests library to handle this asynchronous pattern:
import time
import requests
def run_transcriber_async(api_token, actor_id, payload):
trigger_url = f"https://api.apify.com/v2/acts/{actor_id}/runs?token={api_token}"
response = requests.post(trigger_url, json=payload)
if response.status_code != 201:
raise Exception(f"Failed to start Actor: {response.text}")
run_data = response.json()["data"]
run_id = run_data["id"]
dataset_id = run_data["defaultDatasetId"]
print(f"Started run {run_id}. Dataset ID: {dataset_id}")
status_url = f"https://api.apify.com/v2/acts/{actor_id}/runs/{run_id}?token={api_token}"
while True:
status_response = requests.get(status_url)
if status_response.status_code != 200:
raise Exception(f"Failed to fetch run status: {status_response.text}")
run_status = status_response.json()["data"]["status"]
print(f"Current status: {run_status}")
if run_status in ["SUCCEEDED", "FAILED", "ABORTED", "TIMED-OUT"]:
break
time.sleep(10)
if run_status != "SUCCEEDED":
raise Exception(f"Actor run ended with status: {run_status}")
dataset_url = f"https://api.apify.com/v2/datasets/{dataset_id}/items?token={api_token}"
items_response = requests.get(dataset_url)
return items_response.json()
How do you handle expired WebVTT subtitle URLs?
The subtitleUrl provided in the output payload points directly to TikTok's content delivery network, but this URL is temporary and will expire after a short period, rendering direct downloads impossible later. You must inspect the subtitleUrlExpiresAt timestamp and download the file immediately if your pipeline depends on archiving the raw source tracks.
The plain-text transcript and parsed timing segments are stored safely inside the Apify dataset, which remains accessible according to your storage retention rules. However, the raw subtitleUrl contains cryptographic tokens appended by TikTok's CDN that expire. If your architecture relies on lazy-loading the WebVTT file when a user requests it inside your application, those links will return HTTP 403 Forbidden errors within hours.
To handle this, your ingestion worker must immediately download the WebVTT payload during the initial processing phase and save it to an internal storage bucket or database column.
import axios from 'axios';
async function archiveSubtitleFile(row, storageBucketClient) {
const expiresAt = new Date(row.subtitleUrlExpiresAt).getTime();
const now = Date.now();
if (now >= expiresAt) {
throw new Error(`Cannot archive subtitle for ${row.postId}: URL has already expired.`);
}
try {
const response = await axios.get(row.subtitleUrl, { responseType: 'text' });
const vttContent = response.data;
const destinationKey = `subtitles/${row.postId}_${row.languageCode}.vtt`;
await storageBucketClient.upload({
Key: destinationKey,
Body: vttContent,
ContentType: 'text/vtt'
});
return destinationKey;
} catch (error) {
console.error(`Failed to download VTT file from ${row.subtitleUrl}`, error);
throw error;
}
}
Why should you never rely on input schema prefill values in API calls?
The prefill values specified in an Actor's input schema are designed for the interactive Console UI on the Apify platform and are completely ignored when you invoke runs through direct API calls or existing Actor tasks. When building programmatic integrations, you must explicitly pass every parameter in your JSON payload rather than assuming the platform will fall back to UI-centric prefill defaults.
For example, when looking at the input schema for the TikTok Transcript Scraper, optional fields might appear to have helpful baseline formats or targets pre-populated when you look at the Console web page. But when running this Actor via an API call, only the programmatic default values (like useWhisperFallback defaulting to false) are applied by the runtime engine if omitted.
To guarantee repeatable runs, your API integrations should always construct a complete, fully declared configuration payload. Do not rely on implicit omissions.
Here is a complete, explicit JSON input payload that defines every critical controller option:
{
"postUrls": [
"https://www.tiktok.com/@natgeo/video/7637581966396656909"
],
"postIds": [],
"languages": [
"eng-US",
"spa"
],
"useWhisperFallback": false
}
When do residential and datacenter proxy sessions time out?
Residential proxy sessions rotate and expire after approximately 30 minutes, whereas datacenter proxy sessions can persist for up to 26 hours. If your script runs a large batch of videos sequentially, a hardcoded residential session ID will expire mid-run, resulting in socket dropouts and failed requests. To prevent this, split large runs into smaller batches or use datacenter proxies with country-specific targeting.
Because the TikTok Transcript Scraper processes videos sequentially to avoid aggressive anti-scraping triggers, a large list of URLs can easily keep the Actor container running past the 30-minute mark. If you bind the entire run to a single residential session ID, the target website will start seeing different or dropped IP signatures halfway through, causing requests to fail.
To mitigate this behavior when scraping large volumes, do not hardcode a single session parameter in your network clients, and use datacenter proxies with target country-level granularity (such as targeting US states using the syntax country-US_XX) if you need consistent, long-lived sessions.
What are the limits and expiration rules of unnamed Apify storage?
Unnamed storages on the Apify platform expire automatically, and on the Free plan, only the 10 most recent runs are retained for a maximum duration of 4 months. If your scraper saves transcripts to a default unnamed dataset, you face silent data loss once these platform retention limits are reached. To protect your data, you must use named storages, which are exempt from deletion, or immediately save results to your own database.
If your pipeline relies on querying old run history, checking logs, or verifying previously extracted transcripts, relying on default unnamed datasets will lead to silent data loss. Named storages, on the other hand, are exempt from this automated deletion policy.
Here is a Python example showing how to copy results from an unnamed dataset to an internal PostgreSQL database before the platform's retention sweep removes them:
import psycopg2
def persist_transcripts_to_db(items, db_connection_string):
conn = psycopg2.connect(db_connection_string)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS tiktok_transcripts (
post_id VARCHAR(50) PRIMARY KEY,
username VARCHAR(100),
caption TEXT,
language_code VARCHAR(10),
transcript TEXT,
scraped_at TIMESTAMP
)
""")
for item in items:
if not item.get("success", True):
continue
cursor.execute("""
INSERT INTO tiktok_transcripts (post_id, username, caption, language_code, transcript, scraped_at)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (post_id) DO UPDATE
SET transcript = EXCLUDED.transcript, scraped_at = EXCLUDED.scraped_at
""", (
item["postId"],
item["username"],
item["caption"],
item["languageCode"],
item["transcript"],
item["scrapedAt"]
))
conn.commit()
cursor.close()
conn.close()
How does single-run request queue processing affect scaling?
A request queue on the Apify platform can only be processed by one Actor or task run at a time, preventing you from fanning out execution across multiple parallel runs sharing the same queue. If you attempt to point several scraper runs to a single active queue to speed up processing, they will block each other's progress tracking. To scale horizontally, you must partition your inputs into discrete batches and run them in isolation.
If you have an upstream queue system loaded with thousands of TikTok post IDs and attempt to scale processing horizontally by launching parallel instances of the TikTok Transcript Scraper pointing to that same queue, they will block or overwrite each other's progress tracking.
To achieve horizontal scaling, you must explicitly partition your input list at the database layer before invoking the scraper. Create discrete batches of URLs and pass them as unique postUrls or postIds arrays inside separate Actor runs. This ensures each run operates inside its own isolated scope without resource contention.
Here is a conceptual flow showing how to partition your target URLs in Node.js before launching separate Actor tasks:
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
async function scaleTranscriptScraping(allUrls, batchSize = 10) {
const batches = [];
for (let i = 0; i < allUrls.length; i += batchSize) {
batches.push(allUrls.slice(i, i + batchSize));
}
console.log(`Split ${allUrls.length} URLs into ${batches.length} batches.`);
const runPromises = batches.map((batchUrls, index) => {
return client.actor('crawlerbros/tiktok-transcript-scraper').call({
postUrls: batchUrls,
languages: ['en']
}).then(run => {
console.log(`Batch ${index} started. Run ID: ${run.id}`);
return run;
});
});
const runs = await Promise.all(runPromises);
return runs.map(r => r.defaultDatasetId);
}
What are the limitations and caveats of this scraper?
The TikTok Transcript Scraper is highly optimized for retrieving public video transcripts, but it cannot access private, deleted, or regionally restricted videos. Furthermore, slideshow posts and videos with zero audio or spoken language will fail to return a standard transcript, outputting instead a structured failure row.
Understanding the boundary conditions of this Actor ensures you do not waste resources attempting to process un-scrapable content. Keep the following technical limitations in mind:
- Slideshow Posts: Because photo slideshows uploaded as TikTok videos typically lack an actual audio stream, they do not possess auto-generated speech recognition tracks. The Actor will emit a failure row indicating that no caption track is available.
-
Private and Regionally Locked Content: This tool runs without account authentication. Any video that requires a specific user login, or is restricted to certain geographic regions that your proxy configuration does not cover, will return a row containing a
successvalue offalse. - Fallback Timing Cost: The Whisper fallback option is highly accurate but introduces significant performance overhead. Processing time increases heavily because the container must download the entire video and run a resource-intensive local automatic speech recognition pipeline. If you have tight processing latency requirements, Whisper should be disabled.
- Web Requests Rate Limits: Storage systems on Apify enforce a rate limit of 60 requests per second per storage object, and 400 requests per second for dataset item pushes. While this scraper processes targets sequentially, massive multi-actor setups must avoid throttling limits by spacing dataset extraction calls appropriately.
How much does running the TikTok Transcript Scraper cost?
The billing model for this Actor is strictly based on the number of events triggered during a run, meaning that your cost scales with the volume of data outputted rather than the container's active processing time.
The pricing structure utilizes Apify's PAY_PER_EVENT model. You are charged for every result event and when starting the Actor. There are no separate compute-time fees, container usage rates, or subscription fees on top of these event prices.
The event costs are split as follows:
- "Actor Start" (apify-actor-start): $0.05 per GB of memory allocated to the run. This event triggers exactly once at the beginning of each execution run.
- "result" (apify-default-dataset-item): This event is charged at a flat rate of $0.005 per event, representing a single result written to the default dataset.
The pricing for the "result" event scales down across Apify's volume tiers as follows:
- FREE: $0.005 per event
- BRONZE: $0.00433 per event
- SILVER: $0.00367 per event
- GOLD: $0.003 per event
- PLATINUM: $0.003 per event
- DIAMOND: $0.003 per event
Because the pricing is per-event, your total bill is highly sensitive to the inputs you send to the scraper. Specifically, the scraper outputs one dataset row per video per language. If you scrape a video with 6 translated subtitles and do not specify a language filter, the scraper outputs 6 dataset items, charging you for 6 "result" events. Filtering your input to only the required languages (e.g., ["en"]) directly limits your dataset row count and lowers your run cost.
Similarly, while useWhisperFallback increases processing time because of long audio transcriptions, it does not directly impact the event-based charge itself, since you are still billed per output row. However, remember that if the fallback successfully generates a transcript where none existed natively, you will be billed for a "result" event instead of a simple failure row. Ensure you optimize your container memory settings, since allocating unnecessary GBs of memory to standard runs will inflate your "Actor Start" event costs.
The Actor's README is the source of truth for its inputs, outputs and limits. Need a hand wiring this into your stack? Email info@crawlerbros.com
Top comments (0)