Skip to content

Horizon instance page can time out due to Neutron port list using tenant_id #4000

Description

Summary

In a production Atmosphere deployment, Horizon's Project -> Compute -> Instances page can return a 504 after about 180 seconds even for projects with a small number of instances. The direct slow dependency is Neutron port listing from Horizon: some Horizon code paths pass tenant_id into openstacksdk port list calls, which is dramatically slower than the equivalent project_id filter in this environment.

This appears to be a Horizon/openstacksdk filter compatibility/scaling issue rather than a Nova, Cinder, Glance, Horizon pod health, or project resource-count issue.

Environment

Observed deployment versions:

horizon image: ghcr.io/vexxhost/horizon:2025.1@sha256:35a079c3b9441190f1379d301d9c8ca84359c5457fa9ef209e1b8e3d9dcf7380
horizon: 25.3.2.dev5
openstacksdk: 4.4.0
python-novaclient: 18.9.0
python-neutronclient: 11.4.0
Django: 4.2.19

Relevant Horizon settings are at defaults/enabled in the deployment:

OPENSTACK_INSTANCE_RETRIEVE_IP_ADDRESSES = True
OPENSTACK_NEUTRON_NETWORK['enable_fip_topology_check'] = True

The deployment config only overrides SESSION_TIMEOUT; it does not override these Horizon Neutron settings.

Observed behavior

A fresh Horizon request to /project/instances/ was retried by ingress across all three Horizon pods. Each upstream hit the 60 second read timeout, and ingress returned a final 504 after about 180 seconds:

GET /project/instances/ -> 504 after ~180s
upstream timings: 60.001s, 60.005s, 60.002s

The Horizon pods later logged 200 for the same request, so the pods were not crashing. Horizon was completing the render after NGINX had already given up.

Backend evidence

During the same windows, Neutron logs showed broad list calls from Horizon pod IPs:

GET /v2.0/ports    -> ~8.18 MB response, 32-36s
GET /v2.0/networks -> ~340 KB response, ~2-5s
GET /v2.0/subnets  -> ~1.04 MB response, ~5-8s

Nova, Cinder, Glance, and project-scoped Neutron calls were small and fast. Example project-scoped calls for an affected project were on the order of tens to hundreds of milliseconds.

A direct timing test inside a Horizon pod showed the key difference:

project A:
  ports(tenant_id=...)  -> 47725ms, 7 ports
  ports(project_id=...) ->    99ms, 7 ports

project B:
  ports(tenant_id=...)  -> 46964ms, 20 ports
  ports(project_id=...) ->   242ms, 20 ports

Similar testing for networks showed tenant_id was also problematic in raw SDK usage, although Horizon's network_list() already normalizes tenant_id to project_id before calling openstacksdk.

Suspected cause

In the deployed Horizon code, openstack_dashboard/api/neutron.py normalizes tenant_id to project_id in network_list():

if 'tenant_id' in params:
    params['project_id'] = params.pop('tenant_id')

But port_list() does not perform the same normalization:

def port_list(request, **params):
    ports = networkclient(request).ports(**params)

FloatingIpManager uses this path with tenant_id:

port_search_opts = {'tenant_id': tenant_id}
ports = port_list(self.request, **port_search_opts)

At this cloud scale, the tenant_id port-list path effectively behaves like a broad port scan, while project_id remains correctly scoped and fast.

Impact

This can make Horizon's instance page unusable for affected projects. Because the request goes through ingress retries, the user sees a long hang followed by a 504, even though the underlying OpenStack APIs are mostly healthy.

Proposed fix direction

Normalize tenant_id to project_id in Horizon's Neutron port-list wrapper before calling openstacksdk, matching the existing network_list() behavior:

if 'tenant_id' in params:
    params['project_id'] = params.pop('tenant_id')

Also audit other Horizon Neutron wrappers that pass tenant_id into openstacksdk list calls.

Mitigation notes

Increasing ingress/Horizon timeouts would only hide the symptom and keep the broad Neutron scans. A temporary mitigation may be possible by disabling high-cost Horizon Neutron/floating IP enrichment paths, but the durable fix should avoid the pathological tenant_id SDK filter path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions