If you are building anything on top of TikTok data - an analytics dashboard, a creator tool, a hashtag tracker, a download utility - you are going to hit a wall the first time you put real users in front of it. That wall is the cost and latency of repeated calls. The fix is caching. The trap is that TikTok data is half immutable, half volatile, and treating it uniformly is how you ship a product that shows three-day-old follower counts or, worse, a deleted video that should have been a 404.
This post is a practical deep dive into caching TikLiveAPI responses: what to cache, what to never cache, how to design keys, how to set TTLs per endpoint, how to layer in-memory and Redis (and CDN), how to handle negative responses, and how to warm and invalidate. Code examples are Python with redis-py, and we will end with the credit math that makes all of this worth doing.
The essential part is obvious. The TikLiveAPI pricing model is one credit per request. If your app shows the same creator profile to ten thousand people in an hour, and you call /userinfo-by-username/ ten thousand times, you have spent ten thousand credits on data that did not meaningfully change. Cache hit ratios above 90% are normal for read-heavy apps, and that translates directly into credit savings - see pricing for the per-credit cost.
The dangerous part is staleness. TikTok content mutates in three different ways:
/music-info/) almost never change./userinfo-by-username/) - minutes to hours of staleness is usually fine.A good caching layer respects those three categories. A bad one stuffs everything into Redis with a one-hour TTL and calls it done.
Use the response semantics, not the endpoint URL, to decide. Here is a working classification for the 37 endpoints exposed by TikLiveAPI:
/music-info/ - the title, author, duration, play (mp3 URL) are stable. Cache 24 hours./challenge-info-name/ and /challenge-info-id/ - hashtag metadata (cha_name, desc, cover) is stable; user_count and view_count drift but slowly. Cache 4 hours./region-list/ - it is a static dictionary of ISO country codes. Cache 7 days, or just bake it into your app./playlist-info/ - flat metadata (name, creator_unique_id, video_count). Cache 6 hours./collection-info/ - flat metadata (name, creator_username, creator_user_id, video_count). Cache 6 hours./post-detail/ - flat object with aweme_id, play, hdplay, wmplay, music_info, author. Cache 6 hours, but watch deletes (negative caching section below)./user-posts/ - the play_count, digg_count, comment_count on each video are the whole point of polling. 15 minutes max./user-followers/ and /user-following/ - active follower lists shift constantly, and remember that /user-following/ uses the top-level key followings (plural, trailing s) - not following. Pagination on both is via the time param, not cursor. 10 to 15 minutes./post-comments/ for hot videos - 5 minutes. For cold videos, 1 hour./search-video/ sorted by date - never cache more than 5 minutes. The whole point of the query is freshness.The single most common bug in TikTok-data caching is key collisions. Two requests that should produce different responses get the same cache key, and one user sees another user's paginated cursor results.
Build keys from a canonical form of the endpoint path plus a sorted, normalized query string. Here is a helper:
import hashlib
from urllib.parse import urlencode
def cache_key(endpoint: str, params: dict, scope: str = "global") -> str:
# Sort params so /user-posts/?userid=123&count=10 and
# /user-posts/?count=10&userid=123 produce the same key.
normalized = urlencode(sorted(params.items()))
raw = f"{scope}:{endpoint}?{normalized}"
digest = hashlib.sha1(raw.encode()).hexdigest()[:16]
return f"tla:{scope}:{endpoint.strip('/')}:{digest}"
The scope argument matters. Most TikTok endpoints return public data and should use scope="global" - one cached /userinfo-by-username/?username=mrbeast serves every user of your app. But if your app injects per-user filters (favorites, blocklists, regional fallbacks), use scope=user:{user_id} to keep results isolated.
A working baseline that I have tuned across several production deployments:
TTL_SECONDS = {
"/userinfo-by-username/": 3600, # 1h - nickname/bio drift slowly
"/userinfo-by-id/": 3600, # 1h - same shape, different lookup
"/userid/": 86400, # 24h - username -> id is near-permanent
"/user-posts/": 900, # 15m - counters move
"/user-followers/": 900, # 15m - paginated by 'time'
"/user-following/": 900, # 15m - top key 'followings'
"/post-detail/": 21600, # 6h - flat snake_case object
"/post-comments/": 300, # 5m hot, see override
"/post-comment-replies/": 600, # 10m
"/music-info/": 86400, # 24h - mostly immutable
"/music-posts/": 1800, # 30m
"/challenge-info-name/": 14400, # 4h
"/challenge-info-id/": 14400, # 4h
"/challenge-posts/": 900, # 15m - trending velocity
"/search-video/": 300, # 5m
"/search-user/": 1800, # 30m
"/region-list/": 604800, # 7d - static
}
Note the /userid/ entry. The username-to-numeric-id mapping rarely changes (TikTok does not let users reuse handles freely), so caching the result of /userid/ for a day saves a credit on almost every downstream call that needs userid as input.
For a single process - especially a serverless function on a warm container - an in-memory cache catches the same request hitting the same instance within seconds. Use cachetools.TTLCache:
from cachetools import TTLCache
from threading import Lock
_local = TTLCache(maxsize=2048, ttl=60)
_local_lock = Lock()
On serverless (AWS Lambda, Cloudflare Workers, Vercel), this layer only helps within the lifetime of one warm instance. The moment your function scales to a cold start, this cache is gone. That is fine - it is layer one, not the only layer.
Redis is the durable layer. All instances share it. Use a key prefix for namespacing and EXPIRE (or SET ... EX) for the TTL. Pick a managed Redis (Upstash, Elasticache, Memorystore) if you are serverless so you do not pay for an idle instance.
If you are serving public read-only TikTok data to a web frontend - for example, a hashtag explorer page - put a CDN in front. Cloudflare or Fastly will cache by URL. The price you pay is that CDN caches are global and you cannot easily vary by authenticated user, so only do this for endpoints that have no per-user state.
Here is a complete wrapper. It uses layer 1 + layer 2, returns the verified response shape (nested user and stats camelCase objects), and honors negative caching.
import json
import time
import requests
import redis
from cachetools import TTLCache
from threading import Lock
API_BASE = "https://api.tikliveapi.com"
API_KEY = "YOUR_TIKLIVEAPI_KEY" # send via X-Api-Key header
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
_local = TTLCache(maxsize=2048, ttl=60)
_lock = Lock()
NEG_TTL = 300 # 5 minutes for 404s
POS_TTL = 3600 # 1 hour for user info
NEG_MARKER = "__404__"
def get_user_info(username: str):
key = f"tla:global:userinfo-by-username:{username.lower()}"
# Layer 1: in-process
with _lock:
if key in _local:
cached = _local[key]
return None if cached == NEG_MARKER else cached
# Layer 2: Redis
raw = r.get(key)
if raw is not None:
if raw == NEG_MARKER:
with _lock:
_local[key] = NEG_MARKER
return None
data = json.loads(raw)
with _lock:
_local[key] = data
return data
# Miss: hit TikLiveAPI
resp = requests.get(
f"{API_BASE}/userinfo-by-username/",
params={"username": username},
headers={"X-Api-Key": API_KEY},
timeout=10,
)
if resp.status_code == 404:
r.set(key, NEG_MARKER, ex=NEG_TTL)
with _lock:
_local[key] = NEG_MARKER
return None
resp.raise_for_status()
data = resp.json()
# Verified shape: data["user"] (camelCase: uniqueId, nickname, secUid, verified)
# data["stats"] (camelCase: followerCount, heartCount, videoCount)
r.set(key, json.dumps(data), ex=POS_TTL)
with _lock:
_local[key] = data
return data
A few details worth flagging. The cache key is lowercased on the username because TikTok handles are case-insensitive; otherwise MrBeast and mrbeast get two cache entries. The response is stored as JSON in Redis, which makes it readable from any other service in your stack. And we use one named TTL per response class instead of magic numbers, which makes it easy to tune later.
When a user looks up a deleted account or a removed video, TikLiveAPI returns a 404. If you do not cache that 404, every subsequent request for the same deleted resource costs a credit and slams the upstream. Cache 404s for 5 minutes with a sentinel value (__404__ in the example above) so you can distinguish "cached miss" from "never fetched".
Do not cache 5xx errors. A 502 from upstream is transient and should be retried, not memorized.
For UX-facing apps, latency matters more than perfect freshness. The stale-while-revalidate pattern serves the cached value immediately and refreshes in the background:
import threading
SWR_FRESH = 900 # 15m considered fresh
SWR_STALE = 3600 # serve stale up to 1h while refreshing
def get_with_swr(username: str):
key = f"tla:swr:userinfo:{username.lower()}"
raw = r.get(key)
if raw:
payload = json.loads(raw)
age = time.time() - payload["fetched_at"]
if age < SWR_FRESH:
return payload["data"]
if age < SWR_STALE:
# serve stale, refresh in background
threading.Thread(
target=_refresh, args=(username, key), daemon=True
).start()
return payload["data"]
return _refresh(username, key)
def _refresh(username, key):
fresh = get_user_info(username)
if fresh is not None:
r.set(key, json.dumps({
"data": fresh,
"fetched_at": time.time(),
}), ex=SWR_STALE)
return fresh
On serverless this gets tricky - background threads die when the request handler returns. The fix is to push the refresh to a queue (SQS, Cloud Tasks) or to use a platform-native background job. Do not rely on threading.Thread in a Lambda.
If your app has a known hot set - top 1000 creators in your niche, this week's trending hashtags - warm them into Redis before users ask. A simple scheduled job:
def warm_hot_creators(usernames):
for u in usernames:
try:
get_user_info(u) # writes to Redis as side effect
except Exception as e:
print(f"warm failed for {u}: {e}")
Run it on a cron every 45 minutes (15 minutes shorter than the 1h TTL so users never see a miss). Pair this with /challenge-info-name/ warming for trending tags - you can verify what is trending with /search-challenge/ first.
Three patterns cover almost every invalidation need:
r.delete(key).tla:v2:...). Old keys expire naturally, new keys do not collide.TikLiveAPI does not push webhooks, so all invalidation is pull-based - you decide on TTLs and warming cadence, the API does not tell you when something changed.
cursor (or time for followers/following) is a server-side position marker - caching it across users will serve someone else's offset. Cache the result for a key that includes the cursor, never the cursor itself as a global value.play / hdplay URLs forever. TikTok's CDN URLs on /post-detail/ (the play, wmplay, hdplay fields) are signed and expire. Cache the metadata, but treat the playback URL as having an effective TTL of a few hours and refetch when you get a CDN 403.cid for comments. The /post-comments/ endpoint identifies each comment with an id field, not cid. Build your cache keys on the right field.Concrete example. A creator-analytics dashboard with 5,000 daily active users, each viewing 8 profile pages. That is 40,000 profile views per day. Each profile page calls /userinfo-by-username/ and /user-posts/ - so 80,000 raw API calls per day at one credit each.
With a 1h TTL on user info and 15m TTL on posts, assume realistic hit rates of 92% on user info (a profile is viewed many times per hour) and 70% on posts. Daily savings:
/userinfo-by-username/: 40,000 calls, 92% hit = 3,200 actual requests. Saved: 36,800 credits./user-posts/: 40,000 calls, 70% hit = 12,000 actual requests. Saved: 28,000 credits.Total: 64,800 credits saved per day, or roughly 1.95M credits per month. At TikLiveAPI's pay-as-you-go pricing that is the difference between a viable product and a P&L problem.
/userid/ permanently?Almost. TikTok handles are not freely reusable, and the numeric id for a given username is stable in practice. Cache for 24 hours and refresh on demand if you ever get a 404 on a downstream call that used that id.
/user-followers/?Almost certainly your key includes the pagination time parameter, which differs every call. That is correct (each page is a different cache entry), but follower lists also change frequently, so even within a short window you may see new data. Use a 10 to 15 minute TTL and accept that follower pagination has a low hit rate.
hdplay URL from /post-detail/?Cache the full response object for 6 hours, but treat the play, wmplay, and hdplay CDN URLs as having an effective shelf life - if your downstream download client gets a 403, evict the cache entry and refetch. The metadata (counts, music_info, author) is still useful when the URL is dead.
/post-comments/ by parent comment for replies?Yes, but use a different endpoint - /post-comment-replies/ takes both video_id and comment_id and returns the same envelope (comments, total, cursor, hasMore) where each reply uses id as its identifier. Cache key should include both ids.
Expected behavior. Layer 1 (LRU) only catches hits within a warm container; Layer 2 (Redis) is what gives you cross-instance persistence. On Lambda or Cloudflare Workers, your hit rate will be lower than on a long-running VM, but Redis still saves you most of the credit bill. Avoid background-thread refreshes on serverless - use a queue or a scheduled job instead.
Caching is the single highest-leverage decision you make when building on a metered API. Get the TTLs right, layer your stores, cache your 404s, warm your hot set, and the same dashboard that would have cost you a million credits a month will cost you fifty thousand. Start with the patterns above, then tune to your traffic - and if you want to see the exact response shapes before you wire any of it up, the playground and the per-category docs for users, posts, and music let you inspect every field that this post mentions. Questions, edge cases, or a weird invalidation bug? Contact us or check your usage in your profile. For more deep dives, browse the blog.
Ready to put what you read into code? Try our endpoints live or grab the full reference.