Configuration¶
Everything is configured through environment variables. They are also read
from .env and .env.local in the working directory (later files win, real
environment variables win over both). Names are case-insensitive; booleans
accept true/false, 1/0, yes/no, on/off.
The source of truth is
settings.py.
Presets
compose.yaml loads one of
deploy/presets/*.env
(standard, full, minimal) before your .env. See
Quickstart.
Overview¶
Server¶
HOST¶
Interface the HTTP server (and the metrics listener) binds to. Keep
0.0.0.0 inside containers; use 127.0.0.1 for a bare-metal instance behind
a local reverse proxy.
PORT¶
Port of the HTTP server. The Docker image listens on 3000; publish it with
-p 8000:3000.
WEB_CONCURRENCY¶
Number of worker processes. Defaults to the number of CPUs available to the process, capped at 4. Each worker has its own event loop and, without Redis, its own memory cache — so use Redis when running more than one.
FORWARDED_ALLOW_IPS¶
Comma-separated proxy addresses whose X-Forwarded-For / X-Forwarded-Proto
headers are trusted, or * to trust any (only when the port is not reachable
directly). This determines the client IP used by the rate limiter. Example for
nginx on another host: FORWARDED_ALLOW_IPS=10.0.0.5.
ACCESS_LOG¶
Log every request (true) — off by default to keep logs quiet at high
request rates.
GRACEFUL_TIMEOUT¶
Seconds a worker may take to finish in-flight requests after SIGTERM
before it is killed.
WORKERS_MAX_RSS_MB¶
Recycle a worker whose resident memory exceeds this many MiB. A safety net
for memory-constrained hosts; unset by default. A worker uses ~60 MiB idle and
~100 MiB under load, plus up to MEMORY_CACHE_MAX_MB for its in-process
cache, so a value of about MEMORY_CACHE_MAX_MB + 150 only fires on real
trouble.
METRICS_PORT¶
Expose Prometheus metrics of the server on this port at /metrics. See
Service endpoints.
API¶
DISABLE_DOCS¶
Hide the landing page (/), the API reference (/api), /openapi.json,
the documentation (/docs/) and /sitemap.xml, and on /graphql the
GraphiQL IDE, GET queries and schema introspection. /robots.txt then
disallows everything. See Service endpoints.
CORS_ALLOW_ORIGINS¶
JSON list of allowed browser origins, e.g.
CORS_ALLOW_ORIGINS='["https://example.com"]'. The default ["*"] allows
any origin.
PUBLIC_URL¶
Public address of the instance, scheme and host without a path or trailing
slash, e.g. https://tme.example.com. The absolute URLs in /robots.txt and
/sitemap.xml start with it. Unset, they use the address each request came
in on, which behind a proxy is right only when it passes the original Host.
The deploy workflow sets it from PUBLIC_URL or
PUBLIC_HOST.
DOCS_SITE_DIR¶
Directory of the documentation built for /docs/
(scripts/build_docs.py),
served there by the app. The Docker image ships the build and sets
/app/docs. Unset (an install from source), /docs/… redirects to the same
page of the published documentation. See
Service endpoints.
Features¶
ENABLE_PREVIEWS¶
Enable /v1/preview/{channel} and /v1/previews. When disabled they answer
503.
ENABLE_FEED¶
Enable POST /v1/feed (and its background refresher). Off by default
because it is by far the heaviest endpoint. When disabled it answers 503.
Protection¶
Both are off by default; see Rate limit and proof of work for how they work and what clients send.
ENABLE_RATE_LIMIT¶
Per-client-IP rate limit for every endpoint (GCRA): a burst of
RATE_LIMIT_BURST requests, then
RATE_LIMIT_RPM per minute. Over the limit the API answers
429 with Retry-After. Behind a proxy, set
FORWARDED_ALLOW_IPS so the real client IP is used.
Requests from the server itself (over loopback and not forwarded), such as
health checks and the deploy's smoke test, are not limited.
RATE_LIMIT_RPM¶
Sustained requests per minute per client IP.
RATE_LIMIT_BURST¶
Requests a client may make at once before the per-minute rate applies.
Defaults to RATE_LIMIT_RPM.
RATE_LIMIT_SHARED¶
With REDIS_URL, count each client across all workers and
servers: every process adds its counts to Redis once a second and blocks
clients over the limit there. Requests never wait for Redis. With false, or
without Redis, each process limits on its own.
ENABLE_POW¶
Require a proof of work on every request
(docs, /healthz and the challenge endpoint excepted). Needs
POW_SECRET; GET /v1/pow/challenge hands out challenges.
POW_SECRET¶
Signs challenges (HMAC-SHA256). At least 32 characters, the same on every
server, and secret: with it anyone can forge challenges. Generate one with
openssl rand -hex 32. Changing it invalidates outstanding challenges.
POW_DIFFICULTY¶
Leading zero bits each request's SHA-256 must have, from 8 to 32. Every bit doubles the client's work: 18 is about 0.2 s in Python and 17 ms in native code per request. See Choosing the difficulty.
POW_CHALLENGE_TTL¶
Seconds a challenge stays valid, from 30 to 3600. A client reuses one challenge for all its requests in that time.
POW_BIND_IP¶
Accept a challenge only from the IP it was issued to, so solved challenges cannot be handed out. Turn off for clients whose address changes often.
GraphQL¶
See GraphQL for how the limits are applied.
ENABLE_GRAPHQL¶
Serve the GraphQL API at /graphql. When disabled the
path answers 404. GraphiQL, GET queries and introspection additionally
follow DISABLE_DOCS.
GRAPHQL_MAX_DEPTH¶
Deepest allowed nesting of selections below a root field (the root field is depth 0; introspection fields are not counted).
GRAPHQL_MAX_COMPLEXITY¶
Highest allowed operation cost: 1 per selected field plus 10 per channel a root field may fetch from t.me.
GRAPHQL_MAX_ALIASES¶
Most aliases allowed in one document.
GRAPHQL_MAX_TOKENS¶
Longest accepted document, in GraphQL lexer tokens; bounds parsing work.
GRAPHQL_MAX_BATCH_SIZE¶
Most operations accepted in one batched request (a JSON array). Larger
batches are rejected with 400.
Performance¶
CACHE_MODE¶
| Value | Page cache | Cache-Control for Cloudflare |
|---|---|---|
off |
none — every request hits t.me | none |
normal |
10 s | public, max-age=10, stale-while-revalidate=10 |
aggressive |
120 s | public, max-age=120, stale-while-revalidate=240 |
Applies to /v1/body, /v1/post and /v1/more. Previews and the feed have
their own settings below.
PARSER_MODE¶
| Value | Extracted |
|---|---|
full |
everything: text with entities, media, polls, buttons, replies, link previews, reactions |
simplified |
text (no entities), media and buttons — noticeably cheaper on CPU |
MEMORY_TRIM_INTERVAL¶
Seconds between returns of freed heap memory to the operating system
(malloc_trim, glibc only — the Docker image). After a burst of traffic glibc
otherwise keeps the memory of large short-lived objects for reuse: a worker
under load held ~80 MiB of such free heap. One trim takes about 1 ms. 0
disables it.
Upstream¶
TELEGRAM_BASE_URL¶
Origin the t.me pages are fetched from, scheme://host[:port] without a path.
Point it at a mirror, or at a fake t.me: the integration tests run the image
against tests/integration/fake_telegram.py this way.
TELEGRAM_TIMEOUT¶
Total timeout in seconds for one request to t.me (the connect phase is capped at 5 s). A failed request is retried once on a fresh connection when the error is a network error.
TELEGRAM_MAX_CONCURRENCY¶
Maximum simultaneous requests to t.me per worker process. Protects the server from being rate-limited by Telegram during large feed or previews requests. The slots are shared fairly: round-robin over channels and feed or previews requests, user requests before background refreshes (see Caching).
TELEGRAM_CLIENT¶
HTTP client for requests to t.me:
browser(default): native (wreq on BoringSSL). The TLS handshake, the HTTP/2 settings and the request headers all come fromTELEGRAM_BROWSER_PROFILE, so they describe the same browser.reqwest: pyreqwest (rustls and hyper). It sends the same profile's headers, but its TLS and HTTP/2 fingerprints are those of rustls and hyper, which no browser has, and it keeps no cookies. Not used in production.
The measurements are in HTTP client.
TELEGRAM_BROWSER_PROFILE¶
The browser the requests claim to be. The default, chrome_156, is the
project's own profile of the current stable Chrome, kept in
rust/telegram-me-native/src/http/chrome.rs. The Chromium profiles of
wreq-util, such as chrome_149,
edge_148 or opera_131, can be chosen too. An unknown name stops the server
at startup with the newest Chrome profile in the message.
TELEGRAM_BROWSER_PLATFORM¶
The platform of that browser: android (default), ios, windows, macos
or linux. It sets the User-Agent, sec-ch-ua-platform and
sec-ch-ua-mobile; the handshake is the same on every platform. chrome_156
has no ios variant, because Chrome on iOS uses Apple's TLS stack, not
BoringSSL; the server refuses to start with that pair.
TELEGRAM_SESSION_LIFETIME¶
Seconds one browser identity of the browser client lives, ±25 % at random.
An identity is the cookie jar (t.me sets stel_ssid), the connections and
their TLS session tickets. When it ends, the client starts with an empty jar
and new connections, like a fresh browser profile. The default is 14400
(4 hours, so 3 to 5); 0 keeps an identity until its cookies expire (t.me's
last 24 hours) or t.me answers a 4xx other than 404. Both of those also end
an identity before its time. Each worker process has its own identity
(see HTTP client).
UPSTREAM_QUEUE_LIMIT¶
How many requests a worker lets wait for a free upstream slot. Further
requests that need t.me are answered 503 with a Retry-After header
instead of queueing. Defaults to 16 per TELEGRAM_MAX_CONCURRENCY slot (512).
MAX_WAITING_REQUESTS¶
How many requests a worker lets wait for upstream work (a fetch or render
someone else started) at once; beyond it, 503 with Retry-After. Bounds the
memory a flood of requests can tie up.
SHARED_COALESCING¶
With REDIS_URL, fetch each page once across all workers and servers: the
process that fetches takes a short lease in Redis and the others wait for its
result instead of asking t.me themselves. Turn it off to coalesce per worker
only.
EARLY_REFRESH_WINDOW¶
Renew a cached page that is still being read within this last fraction of its
freshness (0.1: the last second of CACHE_MODE=normal's 10 s), in the
background, so popular pages never expire under load. 0 disables it.
Cache¶
REDIS_URL¶
Redis or Valkey URL, e.g. redis://valkey:6379/0 or
rediss://user:pass@host:6380/0. When set, the page cache, preview cache,
feed popularity and the refresher leader lock are shared by every worker and
server. Each worker still answers repeated requests from its in-process cache
first (MEMORY_CACHE_MAX_MB,
MEMORY_CACHE_MAX_TTL), so only its misses wait for
a Redis round trip. When unset, the in-process cache is the only one.
A Redis outage never slows requests down for long: commands time out after
REDIS_TIMEOUT, and after three failures in a row within
10 seconds Redis is bypassed for 30 seconds (circuit breaker), the API working
on its in-process cache meanwhile. A single failed command does not trigger
it.
Each worker keeps up to 2 × TELEGRAM_MAX_CONCURRENCY (at least 32)
connections to Redis. A burst of commands beyond that, such as several
100-channel feed requests at once, waits up to REDIS_TIMEOUT for a free
connection; a command that still gets none is skipped as a cache miss without
opening the circuit breaker.
REDIS_TIMEOUT¶
Command timeout in seconds, and the longest a command waits for a free pooled connection. A command slower than this opens the circuit breaker.
REDIS_CONNECT_TIMEOUT¶
Seconds to open a new connection to Redis, TLS handshake included. It is
longer than REDIS_TIMEOUT because a connection to a distant TLS endpoint
takes several round trips: about 0.3–0.7 s at 90 ms. Under the command timeout
every reconnect after an error would time out and reopen the circuit breaker,
so Redis would keep dropping out. Each worker also opens its first connection
at startup, so no request pays for it.
REDIS_HEARTBEAT_INTERVAL¶
Every worker writes its own marker to Redis every this many seconds
(tme:v2:node:<host>:<pid>, with a TTL of three intervals); 0 disables it.
- It keeps a connection open, so requests do not pay for reconnecting to a distant TLS endpoint.
- It notices an outage within one interval.
- It is sent even while the circuit breaker is open: as soon as Redis answers again the breaker closes, without waiting for the rest of the 30 s cooldown.
- The live markers show which processes of which servers are running:
redis-cli --scan --pattern 'tme:v2:node:*'.
PREVIEW_CACHE_TTL¶
Seconds a channel preview stays cached. Unknown channels are cached for 60 seconds regardless.
MEMORY_CACHE_MAX_MB¶
Memory limit of the in-process cache, per worker, with or without Redis:
entries are stored compressed and the limit counts what they really occupy
(value, key and bookkeeping). Least recently used entries are evicted first.
With the default of 64, a worker under heavy traffic stays around 170 MiB
(the whole container around 230 MiB). With Redis, a worker holds only what
was read or written in the last MEMORY_CACHE_MAX_TTL
seconds, usually far less than the limit.
MEMORY_CACHE_MAX_TTL¶
With Redis: the longest a worker keeps its own copy of a cache entry, in seconds. A worker sees an entry that another server (or the feed worker) wrote to Redis at most this long after it was written; the cost is one Redis read per key and worker this often, for keys that are being read. Copies never outlive the Redis entry. Without Redis, and while Redis is unavailable, entries keep their full TTL. See Internals → Caching.
Peers¶
Nodes on one private network can share their work directly: each key is fetched from t.me by one node, its owner, and the others ask it. This works without Redis. See Deployment → Peer layer.
ENABLE_PEERS¶
Turn the peer layer on. Every worker process becomes a node. Needs
PEER_SECRET.
PEERS¶
Where to find other nodes: comma-separated host or host:port entries, such
as PEERS=10.0.0.108,10.0.0.165. A DNS name may resolve to several addresses,
and names are resolved again every 30 seconds. An entry without a port is
tried on every port of PEER_PORT's range. One reachable node
is enough: the others are learned from it. With Redis, nodes also find each
other through their heartbeat markers, so PEERS may stay empty.
PEER_PORT¶
First port of the peer layer, for both UDP (membership) and TCP (requests).
Each worker takes the first free port from PEER_PORT to
PEER_PORT + WEB_CONCURRENCY (one spare for a worker being replaced).
0 lets the OS pick a port; only useful in tests.
PEER_ADVERTISE_ADDR¶
The IP address other nodes reach this one at. By default, the source address of the default route. Inside a container that is the container's own address, so set it to the host's private address there (the production deploy does).
PEER_ALLOW_CIDR¶
Comma-separated networks other nodes may connect from. Connections and
datagrams from anywhere else are dropped before anything is read. The default
is every private and loopback range: 10.0.0.0/8, 172.16.0.0/12,
192.168.0.0/16, 127.0.0.0/8, fc00::/7 and ::1/128.
PEER_SECRET¶
Shared secret of the nodes, at least 32 characters, for example from
openssl rand -hex 32. Every message carries an HMAC-SHA256 tag made with it,
a timestamp and a nonce; forged, replayed and older-than-30-s messages are
refused.
PEER_ENCRYPTION¶
Also encrypt every message (XChaCha20-Poly1305 with a key derived from
PEER_SECRET), for networks you do not trust. All nodes must agree.
PEER_TIMEOUT¶
Seconds a peer has to connect and answer a cache read, or to accept a load, before this node does the work itself. A peer that misses it is avoided for 3 seconds.
Feed¶
All of these only matter with ENABLE_FEED=true. See
Internals → Feed.
FEED_FRESH_TTL¶
A cached channel page younger than this (seconds) is used as is.
FEED_STALE_TTL¶
A cached page older than FEED_FRESH_TTL but younger than this is served
immediately while a background refresh replaces it. Older pages are fetched
before answering.
FEED_REFRESH_MODE¶
| Value | Behaviour |
|---|---|
embedded |
each API process runs the refresher; with Redis only one of them (the leader) does the work |
worker |
the API does not run it; start telegram-me worker separately (needs Redis) |
off |
no background refresh; pages are refreshed only by feed requests |
FEED_REFRESH_MIN_INTERVAL¶
Shortest refresh interval (seconds), used for the most popular channels.
FEED_REFRESH_MAX_INTERVAL¶
Longest refresh interval, used for channels with almost no demand. A channel
with popularity score s is refreshed every
clamp(MAX / (1 + s), MIN, MAX) seconds.
FEED_REFRESH_TOP¶
How many of the most popular channels the refresher looks after.
FEED_POPULARITY_HALF_LIFE¶
Half-life in seconds of a channel's popularity: a feed request counts half as much after this time. Channels whose score drops below 0.05 are forgotten.
FEED_HALF_LIFE_HOURS¶
Hours after which a post's freshness halves in the ranking. Lower values favour the newest posts more strongly.
FEED_DIVERSITY¶
From 0 (pure score order) to 1 (strongest spreading of channels and
subjects). See Internals → Feed.
FEED_COLLAPSE_DUPLICATES¶
Merge the same story posted by several channels into its best post, listing
the others in _duplicates.
Logging¶
LOG_LEVEL¶
DEBUG, INFO, WARNING or ERROR.
LOG_FORMAT¶
text for humans, json for log shippers (one JSON object per line with
time, level, logger, message and any structured extras).