Service-to-service communication: internal addresses and internal-only services.

How services of one application reach each other over the internal address, the RIVERVO_<NAME>_URL variables, services without a public address, several instances, and timeouts and retries with examples for Node.js, Laravel and Python.

6 MIN READ

Services of one application talk to each other directly, without going through the internet. A site can call its own API, a worker can call an internal service — and this traffic is free: it is not counted as traffic.

The examples below use two web services: web, the site visitors open, with a Public address, and api, which only web and the other services call, set to Internal only.

The internal address

Every web service — with a public address or internal only — has an Internal address of the form http://<service-name>:<port>, for example http://api:8080. It is shown on the service's page and on the application's Overview, with a copy button and the hint «Other services of this application reach it here — no internet, no traffic charges.»

  • The port is 8080 for a service built from a repository, or the Port you set for a service from a ready image.
  • Only the services of the same application can reach this address. It does not open from the internet or from other applications.
  • The address uses http://. The connection stays inside the application, so there is no certificate to set up.

Variables with the addresses

You don't need to copy the addresses into your code. Every code service of the application — web services, background workers and scheduled jobs — gets a variable with the internal address of each other web service of the application:

RIVERVO_<NAME>_URL=http://<name>:<port>

NAME is the service's name in capital letters, with dashes turned into _:

ServiceVariable
apiRIVERVO_API_URL=http://api:8080
admin-apiRIVERVO_ADMIN_API_URL=http://admin-api:8080
webRIVERVO_WEB_URL=http://web:8080
search (from a ready image, port 7700)RIVERVO_SEARCH_URL=http://search:7700
  • A service does not get a variable for itself.
  • Databases and Valkey keep their own RIVERVO_<NAME>_URL with the connection address. Service names are unique within an application, so the names never clash.
  • The variables are listed on the Variables tab among the variables Set automatically. As with every automatic variable, a variable of your own (or a shared variable) with the same name takes its place — see Environment variables.
  • Variables reach a service with its next deploy. When you add a new web service or rename one, the other services get the new or changed variable on their next deploy. Deploy them again so they pick it up, for example with rivervo deploy <app> --no-build — it starts the running version again with the new variables, without a new build.

Internal-only services

A web service that only other services of the application call — an API behind your site, an admin backend, a search engine — doesn't need to be on the internet. Set it to Internal only:

Public addressInternal only
Address on the internet https://<name>.rivervo.appyesno
Your own domainsyesno
Branch previewsyesno
Internal address for the other servicesyesyes
Several instances and automatic scaling, logs, monitoring, health checksyesyes

An internal-only service runs like any other service; only the other services of the same application can reach it.

Choosing when you create a service

When you create a web service — including the first service of a new application, from a repository, a template or a ready image — choose Public address or Internal only. Nothing is selected in advance, so the choice is always yours. Background workers, scheduled jobs and databases never have a public address, so there is no choice for them.

Services created before this choice existed keep their public address.

Switching later

The service → Settings → Public address: turn the switch and confirm. Only an Administrator of the application or the account owner can change it. Nothing restarts.

  • Switching to internal only is refused while the service has domains connected — remove them on its Domains tab first. Previews must be turned off first too.
  • Turning the public address back on gives the service the same https://<name>.rivervo.app address. The certificate can take about a minute.
  • Adding a domain to an internal-only service is refused — turn its public address on first.

The Terminal tab works in an internal-only service the same way as in any other.

Asynchronous communication

The calls above wait for an answer. When a service should hand over work and go on — background jobs, events that several services react to — send messages through a queue or a message broker instead: Valkey, NATS or RabbitMQ. See Events and queues.

Several instances

When the service you call runs several instances, its internal name leads to all of its running instances, and connections are spread among them.

A client that keeps its connections open and reuses them (a keep-alive connection pool) may keep sending its requests to the same instance; new connections are spread again. This is normal and needs no setup.

Timeouts and retries

During a deploy, or when automatic scaling stops an instance, a request that is in progress can fail. Two habits make the calls between services reliable:

  • A short timeout — a few seconds, so a call never hangs while the instance it went to is stopping.
  • One or two retries with a short pause, for requests that are safe to repeat — reading data with GET. Don't repeat requests that change data (payments, orders) unless your API can tell a repeated request from a new one.

Example: Node.js

const api = process.env.RIVERVO_API_URL
 
async function getWithRetry(url, attempts = 3) {
  for (let i = 1; ; i++) {
    try {
      const res = await fetch(url, { signal: AbortSignal.timeout(5000) })
      if (res.status < 500 || i === attempts) return res
    } catch (err) {
      if (i === attempts) throw err
    }
    await new Promise((resolve) => setTimeout(resolve, 200 * i))
  }
}
 
const res = await getWithRetry(`${api}/orders`)
const orders = await res.json()

Example: Laravel

Put the address in config/services.php:

'api' => [
    'url' => env('RIVERVO_API_URL'),
],

Then read it with config(). In production the configuration is cached, and env() called outside the configuration files returns nothing:

use Illuminate\Support\Facades\Http;
 
$orders = Http::timeout(5)
    ->retry(2, 200)
    ->get(config('services.api.url').'/orders')
    ->json();

timeout(5) waits at most 5 seconds; retry(2, 200) makes up to 2 attempts with 200 ms between them.

Example: Python

import os
import requests
 
API = os.environ["RIVERVO_API_URL"]
 
orders = requests.get(f"{API}/orders", timeout=5).json()

With retries for GET:

import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
 
API = os.environ["RIVERVO_API_URL"]
 
session = requests.Session()
retry = Retry(total=2, backoff_factor=0.2, allowed_methods=["GET"], status_forcelist=[502, 503, 504])
session.mount("http://", HTTPAdapter(max_retries=retry))
 
orders = session.get(f"{API}/orders", timeout=5).json()

A failed GET — a connection error or a 502, 503 or 504 answer — is repeated up to 2 times, with a short pause that grows each time.

From the command line and Rivervo MCP

  • rivervo apps and the service details show each web service's internal address and whether it has a public address.
  • rivervo services set-public <service> on|off switches the public address. It needs the Administrator role and a token with Full access.
  • In Rivervo MCP the same switch is the set_public_address tool.

See Developer tools for tokens and the command line.

Still stuck?

Chat with a real engineer — median response under 3 minutes, any time of day.

Contact support