Skip to content

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

Variable Default Group
HOST 0.0.0.0 Server
PORT 3000 Server
WEB_CONCURRENCY CPU count, max 4 Server
FORWARDED_ALLOW_IPS 127.0.0.1 Server
ACCESS_LOG false Server
GRACEFUL_TIMEOUT 10 Server
WORKERS_MAX_RSS_MB — Server
METRICS_PORT — Server
DISABLE_DOCS false API
CORS_ALLOW_ORIGINS ["*"] API
PUBLIC_URL — API
DOCS_SITE_DIR — (image: /app/docs) API
ENABLE_PREVIEWS true Features
ENABLE_FEED false Features
ENABLE_RATE_LIMIT false Protection
RATE_LIMIT_RPM 120 Protection
RATE_LIMIT_BURST = RATE_LIMIT_RPM Protection
RATE_LIMIT_SHARED true Protection
ENABLE_POW false Protection
POW_SECRET — Protection
POW_DIFFICULTY 18 Protection
POW_CHALLENGE_TTL 300 Protection
POW_BIND_IP true Protection
ENABLE_GRAPHQL true GraphQL
GRAPHQL_MAX_DEPTH 10 GraphQL
GRAPHQL_MAX_COMPLEXITY 1500 GraphQL
GRAPHQL_MAX_ALIASES 15 GraphQL
GRAPHQL_MAX_TOKENS 2000 GraphQL
GRAPHQL_MAX_BATCH_SIZE 10 GraphQL
CACHE_MODE normal Performance
PARSER_MODE full Performance
MEMORY_TRIM_INTERVAL 10 Performance
TELEGRAM_BASE_URL https://t.me Upstream
TELEGRAM_TIMEOUT 10 Upstream
TELEGRAM_MAX_CONCURRENCY 32 Upstream
TELEGRAM_CLIENT browser Upstream
TELEGRAM_BROWSER_PROFILE chrome_156 Upstream
TELEGRAM_BROWSER_PLATFORM android Upstream
TELEGRAM_SESSION_LIFETIME 14400 Upstream
UPSTREAM_QUEUE_LIMIT 16 × TELEGRAM_MAX_CONCURRENCY Upstream
MAX_WAITING_REQUESTS 4096 Upstream
SHARED_COALESCING true Upstream
EARLY_REFRESH_WINDOW 0.1 Upstream
REDIS_URL — Cache
REDIS_TIMEOUT 0.5 Cache
REDIS_CONNECT_TIMEOUT 2 Cache
REDIS_HEARTBEAT_INTERVAL 5 Cache
PREVIEW_CACHE_TTL 3600 Cache
MEMORY_CACHE_MAX_MB 64 Cache
MEMORY_CACHE_MAX_TTL 10 Cache
ENABLE_PEERS false Peers
PEERS — Peers
PEER_PORT 7946 Peers
PEER_ADVERTISE_ADDR address of the default route Peers
PEER_ALLOW_CIDR private networks Peers
PEER_SECRET — Peers
PEER_ENCRYPTION false Peers
PEER_TIMEOUT 0.05 Peers
FEED_FRESH_TTL 60 Feed
FEED_STALE_TTL 900 Feed
FEED_REFRESH_MODE embedded Feed
FEED_REFRESH_MIN_INTERVAL 30 Feed
FEED_REFRESH_MAX_INTERVAL 600 Feed
FEED_REFRESH_TOP 200 Feed
FEED_POPULARITY_HALF_LIFE 3600 Feed
FEED_HALF_LIFE_HOURS 24 Feed
FEED_DIVERSITY 0.5 Feed
FEED_COLLAPSE_DUPLICATES true Feed
LOG_LEVEL INFO Logging
LOG_FORMAT text Logging

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 from TELEGRAM_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).