Summary
On Appwrite 2.0.0 (self-hosted) with an external Redis that requires a password, every publish through the publisher pool fails with NOAUTH Authentication required. Observed via the task-scheduler's hourly stats-resources task. The credentials are present in the container environment and are correct (verified directly against the Redis server); they are simply never passed to (or used by) the queue connection.
Environment
- Appwrite 2.0.0 (self-hosted, Docker Compose)
- Redis 7.4.7 external (own host, requirepass set)
- _APP_REDIS_HOST, _APP_REDIS_PORT, _APP_REDIS_USER, _APP_REDIS_PASS set on all containers (verified inside the container with docker exec ... env | grep _APP_REDIS)
Observed behavior
appwrite-task-scheduler, every hour:
[StatsResources] Failed to publish stats resources message: NOAUTH Authentication required.
stats_resources_task · 207ms
stats_resources_task.projects_queued 0
stats_resources_task.projects_failed 1
Deterministic — persists across full stack restarts.
Additionally: the combined worker's queue consumers borrow the same publisher pool (app/worker.php line ~100: BrokerPool(publisher: $publisher, consumer: $publisher)), so on passworded Redis, messages pile up in the queue unconsumed as well — verify with LLEN utopia-queue.queue.v1-stats-resources.
Everything else that uses Redis works: realtime pub/sub authenticates and serves, the cache pool works, and queue produce/consume for table attribute creation works end to end when Redis has no password.
Root cause — two layers
Layer 1: app/init/registers.php (2.0.0) — the publisher pool drops the DSN credentials.
The fallback Redis DSN is built with user and pass:
$fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([
'scheme' => 'redis',
'host' => System::getEnv('_APP_REDIS_HOST', 'redis'),
'port' => System::getEnv('_APP_REDIS_PORT', '6379'),
'user' => System::getEnv('_APP_REDIS_USER', ''),
'pass' => System::getEnv('_APP_REDIS_PASS', ''),
]);
…but the publisher adapter constructs the connection with host and port only:
case 'publisher':
return match ($dsn->getScheme()) {
'redis' => (function () use ($dsn) {
$connection = new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort());
return new Queue\Broker\Redis($connection, $connection);
})(),
default => null
};
All other Redis pools (cache, pubsub, lock) go through the shared $resource() factory, which does pconnect + $redis->auth($dsnPass) — which is why those paths authenticate correctly and only the publisher fails.
Layer 2: utopia-php/queue Connection\Redis accepts $user/$password but never authenticates.
Current main of utopia-php/queue, src/Queue/Connection/Redis.php:
public function __construct(protected string $host, protected int $port = 6379,
protected ?string $user = null, protected ?string $password = null,
protected float $connectTimeout = -1, protected float $readTimeout = -1) {}
protected function getRedis(): \Redis
{
// ...
$redis = new \Redis();
$redis->connect($this->host, $this->port, $connectTimeout);
if ($this->readTimeout >= 0) {
$redis->setOption(\Redis::OPT_READ_TIMEOUT, $this->readTimeout);
}
$this->redis = $redis;
return $this->redis;
// ...
}
$this->user and $this->password are never read; there is no auth() call anywhere in the class. Even if Appwrite passed them, the connection would still not authenticate.
Evidence it is not a configuration problem
- Credentials verified inside the running container (docker exec env): _APP_REDIS_PASS, _APP_REDIS_USER, _APP_REDIS_HOST, _APP_REDIS_PORT all present and correct.
- Direct protocol test from the same host: AUTH default <pass> → +OK; PING → +PONG; LPUSH/LPOP succeed. Two-argument AUTH form (used when _APP_REDIS_USER is set) also returns +OK.
- Realtime, cache, and lock pools authenticate against the same server with the same env — only the publisher pool fails.
- Survives full stack restarts; failure is deterministic (the code path never attempts auth).
- With a passwordless Redis, everything works — confirming the bug only manifests with authentication enabled.
Why default installs don't see this
The official docker-compose Redis service runs without requirepass and _APP_REDIS_PASS defaults to empty — the publisher's missing auth is invisible. The bug only manifests for self-hosters pointing at password-protected external Redis.
Suggested fix
utopia-php/queue — in getRedis(), after connect():
if ($this->password !== null && $this->password !== '') {
$this->user
? $redis->auth([$this->user, $this->password])
: $redis->auth($this->password);
}
appwrite — in registers.php, pass the DSN credentials to the publisher connection:
$connection = new Queue\Connection\Redis(
$dsn->getHost(),
$dsn->getPort(),
$dsn->getUser() ?? null,
$dsn->getPassword() ?? null
);
Impact
- Hourly resource gauges (v1-stats-resources) are not published — Usage tab resource stats missing.
- On passworded Redis, ALL queue messages published by the scheduler pile up unconsumed (scheduled functions, stats, etc.) since the worker's consumer also borrows the publisher pool.
- Any other producer using the publisher pool against passworded Redis is affected the same way.
Related
Filed from a live deployment: Appwrite 2.0.0 on Coolify with external PostgreSQL/Redis/ClickHouse. Verified fix working in production: queue depth 2 → 0, gauges 0 → 210 after patching.
Summary
On Appwrite 2.0.0 (self-hosted) with an external Redis that requires a password, every publish through the publisher pool fails with NOAUTH Authentication required. Observed via the task-scheduler's hourly stats-resources task. The credentials are present in the container environment and are correct (verified directly against the Redis server); they are simply never passed to (or used by) the queue connection.
Environment
Observed behavior
appwrite-task-scheduler, every hour:
Deterministic — persists across full stack restarts.
Additionally: the combined worker's queue consumers borrow the same publisher pool (app/worker.php line ~100: BrokerPool(publisher: $publisher, consumer: $publisher)), so on passworded Redis, messages pile up in the queue unconsumed as well — verify with LLEN utopia-queue.queue.v1-stats-resources.
Everything else that uses Redis works: realtime pub/sub authenticates and serves, the cache pool works, and queue produce/consume for table attribute creation works end to end when Redis has no password.
Root cause — two layers
Layer 1: app/init/registers.php (2.0.0) — the publisher pool drops the DSN credentials.
The fallback Redis DSN is built with user and pass:
…but the publisher adapter constructs the connection with host and port only:
All other Redis pools (cache, pubsub, lock) go through the shared $resource() factory, which does pconnect + $redis->auth($dsnPass) — which is why those paths authenticate correctly and only the publisher fails.
Layer 2: utopia-php/queue Connection\Redis accepts $user/$password but never authenticates.
Current main of utopia-php/queue, src/Queue/Connection/Redis.php:
$this->user and $this->password are never read; there is no auth() call anywhere in the class. Even if Appwrite passed them, the connection would still not authenticate.
Evidence it is not a configuration problem
Why default installs don't see this
The official docker-compose Redis service runs without requirepass and _APP_REDIS_PASS defaults to empty — the publisher's missing auth is invisible. The bug only manifests for self-hosters pointing at password-protected external Redis.
Suggested fix
utopia-php/queue — in getRedis(), after connect():
appwrite — in registers.php, pass the DSN credentials to the publisher connection:
Impact
Related
Filed from a live deployment: Appwrite 2.0.0 on Coolify with external PostgreSQL/Redis/ClickHouse. Verified fix working in production: queue depth 2 → 0, gauges 0 → 210 after patching.