Ops Notes

Redfish API Production Best Practices: Blood, Sweat, and Tears from Managing 5000+ Nodes

Infrastructure Visualization

Don’t Believe the “Batteries Included” Hype

Last year I took over a data center migration project spanning 5000+ nodes. The previous team’s Redfish scripts? They could brick half a rack in one run. I’m not exaggerating—a single batch firmware update, lacking proper idempotency checks, turned the entire cluster’s BMCs into paperweights. The ops director’s face was darker than a power outage in the server room.

Redfish API looks simple on the surface—just RESTful + JSON, right? But throw it into production, and the rabbit hole goes deeper than you’d think. This post is the raw, unfiltered collection of every pitfall my team and I have hit over the past two years.

1. Connection Management: Don’t Let Sessions Become Time Bombs

1.1 Session Lifecycle Management

This is the most basic—and most frequently screwed up—part. Redfish’s session mechanism relies on X-Auth-Token, but I’ve seen countless beginners hardcode Basic Auth into every single request.

import requests
import time

# Wrong way: re-authenticate every request
def bad_way(bmc_ip, user, pwd):
    session = requests.Session()
    session.auth = (user, pwd)
    # Every request carries plaintext credentials—exposed in logs
    resp = session.get(f'https://{bmc_ip}/redfish/v1/Systems/1')
    return resp.json()

# Right way: create a session and reuse it
def good_way(bmc_ip, user, pwd):
    session = requests.Session()
    session.verify = False  # Use proper certs in production
    
    # Create session
    login_payload = {
        "UserName": user,
        "Password": pwd
    }
    login_resp = session.post(
        f'https://{bmc_ip}/redfish/v1/SessionService/Sessions',
        json=login_payload
    )
    
    if login_resp.status_code == 201:
        token = login_resp.headers['X-Auth-Token']
        session.headers.update({'X-Auth-Token': token})
        # Set timeouts and retries
        session.timeout = (5, 30)  # connect timeout, read timeout
        return session
    else:
        raise Exception(f"Login failed: {login_resp.status_code}")

Key lesson: Sessions have TTLs, usually 30 minutes by default. We’ve hit cases where sessions expired mid-batch operation, causing partial failures. Solution? Catch 401 status codes and automatically reconnect.

1.2 Connection Pooling

We started with single-threaded queries—40 minutes to poll 5000 nodes. Then we switched to ThreadPoolExecutor and promptly DDoSed our own BMCs. Many low-end BMCs max out at 8 concurrent connections.

from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

# Semaphore to control concurrency
semaphore = threading.Semaphore(6)  # Leave some headroom

def safe_query(bmc_ip, session):
    with semaphore:
        try:
            resp = session.get(
                f'https://{bmc_ip}/redfish/v1/Systems/1',
                timeout=10
            )
            return resp.json()
        except Exception as e:
            log.error(f"{bmc_ip} query failed: {e}")
            return None

# Process in batches
batch_size = 100
for i in range(0, len(bmc_list), batch_size):
    batch = bmc_list[i:i+batch_size]
    with ThreadPoolExecutor(max_workers=8) as executor:
        futures = {executor.submit(safe_query, ip, session): ip 
                  for ip in batch}
        for future in as_completed(futures):
            result = future.result()
            # Process result

2. Idempotency and State Machines: Don’t Repeat Operations

There’s a Reddit thread that really resonated with me—someone asked “how to use Redfish without knowing the server model.” This exposes a core problem: too many people treat Redfish like a simple REST API, ignoring the state machine model underneath.

2.1 Check State Before Operations

def safe_reboot(bmc_ip, session):
    # Get current state
    system_resp = session.get(
        f'https://{bmc_ip}/redfish/v1/Systems/1'
    )
    system = system_resp.json()
    current_state = system.get('PowerState')
    
    if current_state == 'On':
        # Check for ongoing operations
        if system.get('Status', {}).get('State') == 'Starting':
            log.warning(f"{bmc_ip}: System is starting, skip reboot")
            return False
        
        # Execute reboot
        resp = session.post(
            f'https://{bmc_ip}/redfish/v1/Systems/1/Actions/ComputerSystem.Reset',
            json={"ResetType": "ForceRestart"}
        )
        
        if resp.status_code == 204:
            # Poll for completion
            return wait_for_completion(bmc_ip, session, target_state='On')
    else:
        log.info(f"{bmc_ip}: Current state {current_state}, no reboot needed")
        return False

2.2 Task Polling Done Right

Redfish async operations return Task resources. But vendor implementations vary wildly—Dell’s iDRAC can have a 30-second delay in Task status updates.

import time
import backoff

@backoff.on_exception(
    backoff.expo,
    (requests.exceptions.Timeout, requests.exceptions.ConnectionError),
    max_tries=5
)
def wait_for_task(bmc_ip, session, task_url, timeout=300):
    start = time.time()
    while time.time() - start < timeout:
        resp = session.get(f'https://{bmc_ip}{task_url}')
        task = resp.json()
        
        state = task.get('TaskState')
        percent = task.get('PercentComplete', 0)
        
        if state == 'Completed':
            log.info(f"{bmc_ip}: Task completed in {time.time()-start:.1f}s")
            return True
        elif state == 'Failed':
            error = task.get('Messages', [{}])[0].get('Message', 'Unknown')
            raise Exception(f"Task failed: {error}")
        elif state == 'Cancelled':
            raise Exception("Task was cancelled")
        
        # Dynamic polling interval
        if percent > 50:
            time.sleep(2)
        else:
            time.sleep(5)
    
    raise TimeoutError(f"Task did not complete within {timeout}s")

3. Error Handling: Don’t Trust What the BMC Tells You

This is the part that’s driven me up the wall. Different vendors’ Redfish implementations vary so much it’s painful. Here’s our battle-tested summary:

VendorRedfish VersionConsistencyKnown IssuesOur Strategy
Dell iDRAC 91.6-1.12GoodTask status update delayAdd 5s initial wait
HPE iLO 51.0-1.10ModerateSome properties return empty stringsAdd null/empty checks
Supermicro X111.0-1.5PoorNo Redfish Event subscription supportFall back to polling
Inspur1.6-1.8ModerateSome ops return 500 but succeedVerify actual operation outcome
Lenovo XClarity1.8-1.12GoodBMC reboot after firmware update takes foreverSet 600s timeout

3.1 Defensive Parsing

def safe_get_property(obj, path, default=None):
    """Safely access nested properties"""
    keys = path.split('.')
    current = obj
    for key in keys:
        if isinstance(current, dict):
            current = current.get(key)
            if current is None:
                return default
        else:
            return default
    return current

# Usage
system_info = resp.json()
# Don't directly access potentially missing properties
serial = safe_get_property(system_info, 'SerialNumber', 'UNKNOWN')
model = safe_get_property(system_info, 'Model', 'UNKNOWN')
# Some vendors put MemorySummary in different places
memory_gb = safe_get_property(system_info, 'MemorySummary.TotalSystemMemoryGiB', 0)

4. The Art of Batch Operations

4.1 Batching and Retry Strategies

We once tried to push BIOS config changes to 2000 nodes simultaneously. It took down the entire datacenter’s DHCP service—all BMCs rebooted at once, creating a DHCP request storm.

class BatchRedfishManager:
    def __init__(self, bmc_list, max_concurrent=6, batch_size=50):
        self.bmc_list = bmc_list
        self.max_concurrent = max_concurrent
        self.batch_size = batch_size
        self.results = {}
    
    def execute_with_retry(self, operation_func, max_retries=3):
        for batch_num, batch in enumerate(self._batches()):
            log.info(f"Processing batch {batch_num+1}, servers {batch[0]} to {batch[-1]}")
            
            with ThreadPoolExecutor(max_workers=self.max_concurrent) as executor:
                futures = {}
                for bmc in batch:
                    future = executor.submit(
                        self._retry_wrapper,
                        operation_func,
                        bmc,
                        max_retries
                    )
                    futures[future] = bmc
                
                for future in as_completed(futures):
                    bmc = futures[future]
                    try:
                        result = future.result()
                        self.results[bmc] = {'status': 'success', 'data': result}
                    except Exception as e:
                        self.results[bmc] = {'status': 'failed', 'error': str(e)}
            
            # Wait between batches to prevent BMC overload
            if batch_num < len(self.bmc_list) // self.batch_size:
                time.sleep(10)
        
        return self.results
    
    def _retry_wrapper(self, func, bmc, max_retries):
        for attempt in range(max_retries):
            try:
                return func(bmc)
            except (requests.exceptions.ConnectionError, 
                    requests.exceptions.Timeout) as e:
                if attempt == max_retries - 1:
                    raise
                wait = 2 ** attempt  # Exponential backoff
                log.warning(f"{bmc}: Attempt {attempt+1} failed, retrying in {wait}s")
                time.sleep(wait)

5. Security Best Practices

5.1 Credential Management

There’s a Reddit thread asking about “Secret Storage Best Practices.” In the Redfish context, this is critical. BMC passwords are the crown jewels—leak them, and you’ve handed over physical server control.

# Don't do this
BMC_PASSWORD = "admin123"  # Hardcoded in script

# Use environment variables (still not ideal)
import os
password = os.environ.get('BMC_PASSWORD')

# Recommended: Use HashiCorp Vault or Infisical
import hvac

def get_bmc_credentials(bmc_ip):
    client = hvac.Client(url='https://vault.example.com:8200')
    client.token = os.environ['VAULT_TOKEN']
    
    secret = client.secrets.kv.v2.read_secret_version(
        path=f'bmc/{bmc_ip}',
        mount_point='infra'
    )
    
    return {
        'username': secret['data']['data']['username'],
        'password': secret['data']['data']['password']
    }

5.2 Certificate Verification

I know 99% of people still use verify=False in production. But at minimum:

# Minimum security config
session.verify = '/path/to/ca-bundle.crt'  # Use custom CA bundle

# Or use certificate fingerprint verification (good for no-CA scenarios)
import hashlib

CERT_FINGERPRINTS = {
    'bmc-001.example.com': 'sha256$ABC123...',
    'bmc-002.example.com': 'sha256$DEF456...',
}

def verify_bmc(bmc_ip, cert_der):
    fingerprint = hashlib.sha256(cert_der).hexdigest()
    expected = CERT_FINGERPRINTS.get(bmc_ip)
    if expected and fingerprint != expected:
        raise Exception(f"Certificate mismatch for {bmc_ip}")
    return True

6. Monitoring and Alerting

6.1 Key Metrics

Here’s what we ended up monitoring:

MetricRedfish PathAlert ThresholdNotes
CPU Temp/Chassis/{id}/Thermal#/Temperatures>85°CEmergency throttling threshold
Fan Speed/Chassis/{id}/Thermal#/Fans<30% RPMInsufficient cooling
Power Supply/Chassis/{id}/Power#/PowerSuppliesRedundancy lostAlert immediately
Disk Health/Systems/{id}/Storage#/DrivesPredicted FailureHandle within 24h
Memory Errors/Systems/{id}/Memory#/MemoryMetricsCorrectedECC > 100/hrMemory about to fail

7. FAQ

Q: How do you handle Redfish API version compatibility? A: First, GET /redfish/v1 and check the RedfishVersion field. Different versions support different properties. Use @odata.context for dynamic resource discovery. We maintain a version mapping table to decide which properties and endpoints to use based on the version number.

Q: How to prevent BMC overload during batch operations? A: Use a semaphore to cap concurrency (recommend ≤8), add 10-second delays between batches. If you hit ConnectionErrors, immediately reduce concurrency and increase delays. We have an automatic backoff mechanism—3 consecutive errors halves the concurrency.

Q: How do you handle different vendor Redfish implementations? A: Abstract an adapter layer. Each vendor gets a concrete adapter that handles vendor-specific behaviors (Dell’s firmware upgrade flow is completely different from HPE’s). Use a factory pattern to select the right adapter based on the Manufacturer field.

Q: Is Redfish Event subscription reliable? A: No. In our environment, SuperMicro and Inspur BMCs frequently drop events. We use a hybrid approach: Events as a speed-up mechanism, with regular polling as a safety net.

8. Summary

Redfish API looks simple, but production deployment is a minefield. Core lessons:

  • Never trust BMC responses—always do defensive checks
  • Idempotency is rule #1—check state before operations, verify results after
  • Concurrency control is non-negotiable—BMCs are more fragile than you think
  • Vendor differences are huge—an abstraction adapter layer is mandatory
  • Credential management must be strict—BMC password leak = server compromise

References & Community Insights

The following authoritative resources were referenced for architectural best practices and specifications:

Elvin Hui

About the Author: Elvin Hui

Elvin is a Senior Infrastructure Engineer with 10+ years of experience spanning data centers, cloud-native architecture, and network security. Certified in CCNA and AWS Solutions Architecture, I focus on turning real-world production "war stories" into actionable, hardcore technical guides.