Skip to content
Arbinel

Live updates

With live updates on, issue and pull request pages and the Inbox update without a reload when something changes. Forgejo has no equivalent. Its pages show the state at the time they loaded. This page is for site administrators.

[realtime]
ENABLED = true
Key Default Meaning
ENABLED false Turns live updates on.
CONN_STR empty Redis connection string that carries events between nodes. When empty, each node receives only its own events. See Multiple nodes.
MAX_CONNECTIONS 10000 Maximum number of open streams that one node accepts. Values of 0 or less mean 10000.
HEARTBEAT_INTERVAL 25s How often an idle stream sends a keep-alive event. Values under 1s mean 25s.

While live updates are off, the pages behave as in Forgejo.

An open issue or pull request page updates:

  • the title and the header,
  • the timeline: new, edited and deleted comments, reviews, reactions, label, milestone and assignee changes, and pushes,
  • the merge box of an open pull request, including the state of each check.

A merge, close or reopen changes too much of the page to update it in place. The page reloads instead, or asks the user to reload if a reload would lose text they typed. While the user works in a part of the page, such as an open comment editor, the page does not update that part until the user is done.

The Inbox updates the user’s notifications and the check states of their open pull requests.

Arbinel sends server-sent events over one long-lived GET request to /-/realtime. It does not use WebSockets.

An event only states that something changed, such as issue:42. The page then fetches the new state with a normal request, which applies the viewer’s permissions as usual. The stream itself accepts only topics that the viewer can read. On instances with [service] REQUIRE_SIGNIN_VIEW = true, signed-out visitors get no events.

A browser opens one stream for all its tabs, through a shared worker. Browsers without shared workers open one stream per tab and close it while the tab is hidden.

Each stream ends after 30 to 60 minutes, and the browser then reconnects. The reconnect checks the viewer’s permissions again and spreads streams across nodes that joined in the meantime. After a reconnect, the page checks its state once, so it does not miss changes made while it was disconnected.

When a node has MAX_CONNECTIONS open streams, it answers new ones with 503 and a Retry-After header. The page then falls back to polling, every 15 seconds to 5 minutes, and tries the stream again after about a minute.

The /metrics endpoint, when enabled, reports these counters per node:

Metric Meaning
gitea_arbinel_realtime_connections Open streams on this node.
gitea_arbinel_realtime_published_total Topics this node published, after combining bursts.
gitea_arbinel_realtime_delivered_total Events this node sent to its streams.
gitea_arbinel_realtime_dropped_total Events dropped because a stream fell behind. Each one makes the browser check its state.

Forgejo’s reverse proxy examples do not mention long-lived requests. The proxy must meet two requirements for the stream to work.

Buffering turned off. A proxy that buffers responses holds events back until its buffer fills, so pages update late or never. Arbinel sends X-Accel-Buffering: no, which nginx honours by default. Other proxies may need buffering turned off for /-/realtime.

An idle timeout longer than the heartbeat. An idle stream sends an event every HEARTBEAT_INTERVAL, 25 seconds by default. The proxy’s read or idle timeout must be longer, or the proxy closes idle streams and browsers reconnect repeatedly. Forgejo’s HAProxy example uses timeout server 30s, which leaves only 5 seconds of margin. If you raise HEARTBEAT_INTERVAL, also raise the proxy timeout.

For nginx, use a location block like this:

location /-/realtime {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_read_timeout 1h;
}

Over HTTP/1.1, browsers allow only six connections per host, and every open stream takes one of them. With the shared stream, each browser uses only one of them. Serving the instance over HTTP/2 removes the limit.

On a high availability cluster, a change on one node has to reach viewers on every node. Arbinel carries events between nodes over Redis publish and subscribe.

You usually do not need to set CONN_STR. When it is empty, [high_availability] ENABLED = true and [queue] TYPE = redis, Arbinel uses the queue’s CONN_STR.

[high_availability]
ENABLED = true

[queue]
TYPE = redis
CONN_STR = redis://redis.internal:6379/0

[realtime]
ENABLED = true

Redis channels ignore the database number, so Arbinel prefixes each channel with a hash of ROOT_URL. Several instances can share one Redis server without receiving each other’s events.

MAX_CONNECTIONS applies to each node. If a load balancer spreads streams evenly, a cluster of three nodes can hold three times as many streams.