Post-deploy smoke test

April 28, 2026 · View on GitHub

The 90-second checklist to run after every deploy. Catches the boring 30 % of bugs that would otherwise be discovered at 03:14 by an oncall engineer who didn't push the change. None of these tests require user traffic; all of them use the project's own MCP tools.


The checklist

#TestToolPass criteria
1Process is alive and answering MIhealth_check()Returns OK
2SIP listener is bound and receivingsip_options_ping(target="127.0.0.1:5060")succeeded >= 1
3Memory headroom OKperf_memory_report()shmem usage < 70 %
4Process count matches expectationperf_process_breakdown()total_processes == children + tcp_workers + extras
5Dispatcher destinations reachabledispatcher_list_db()Every dest with state=Active
6TLS certs not expiring soontls_check_all_expiry(warn_days=30)expiring=[]
7Anti-flood firing only on noisepike_list_blocked()No legitimate-looking IPs
8Auth path workssynthetic REGISTER + check registrar_stats()accepted_regs increments
9Routing path workssynthetic INVITE via sip_generate_sipp_scenario + check dlg_list()Dialog appears
10Cluster healthy (if applicable)cluster_sync_check(cluster_id=1)healthy: true

If all 10 pass, the deploy is probably good. If 1-3 fail, roll back immediately with cfg_rollback.


Test 1 — health_check

health_check()

Returns {"ok": true, ...} if MI is reachable and responding to which. If MI is dead, every other tool in the checklist will fail too; fix this first.


Test 2 — sip_options_ping

sip_options_ping(target="127.0.0.1:5060", count=3, timeout_sec=2.0)

Pure-Python UDP probe. Validates that the SIP listener is bound, responding, and the process is running normally. If you get "timeout" for all 3, the daemon may have started but failed to bind (port collision, missing capability, address-not-available).

If the proxy listens on multiple sockets (udp:5060, tcp:5060, tls:5061), test each:

sip_options_ping(target="127.0.0.1:5060")
sip_options_ping(target="127.0.0.1:5061", allow_public=False)  # tls

(TCP / TLS probes via this tool aren't yet supported; for those use openssl s_client or the tls_connections() MI call.)


Test 3 — memory

perf_memory_report()

Look at mem.shmem_used versus mem.shmem_total. Healthy production deployment runs at 30-60 % shmem utilisation. > 70 % is a warning;

85 % is a "tune it before this becomes the incident."

If you used cfg_tune to size the deployment and dlg_size_bytes is unset, the calculation may have under-allocated; re-run with a realistic per-dialog size.


Test 4 — processes

perf_process_breakdown()

Group by type; verify the count matches the global config. A common silent failure: tcp_workers=8 was set but the daemon ignored it because of a config syntax error elsewhere, falling back to default. The process count exposes that.


Test 5 — dispatcher / load_balancer

dispatcher_list_db()

Every destination should show state=Active. After deploy, give the ping mechanism one full ds_ping_interval (default 10s) to mark destinations active before judging.

If lb_list() is meaningful for your scenario, run that too; same criterion.


Test 6 — TLS

tls_check_all_expiry(warn_days=30)

If anything is expiring within 30 days, deal with it now (rotate + tls_reload), not when the alert fires. See tls-from-scratch.md for the rotation procedure.


Test 7 — anti-flood

pike_list_blocked()

After deploy, the blocked list should be either empty or only contain hosts you recognise as scanner traffic. If a legitimate carrier IP appears, your pike thresholds are too tight for the change you just shipped. Tune reqs_density_per_unit upward or add an exception.


Test 8 — synthetic REGISTER

If the scenario serves UAs (residential PBX, registrar, multi-tenant, WebRTC, IMS S-CSCF, asterisk-trunking, MSRP gateway, RADIUS auth):

  1. Provision a test user once:

    gen_test_subscribers(domain="<your-domain>", count=1, dry_run=False)
    

    Creates test0001 with password secret0001.

  2. Generate a SIPp REGISTER scenario:

    sip_generate_sipp_scenario(
        kind="register",
        target="<host>:5060",
        from_user="test0001"
    )
    
  3. Run the SIPp command on a separate host. Should see two REGISTERs (challenge + reply) and a 200 OK.

  4. Verify on the proxy:

    ul_show_contacts(table="location", aor="test0001@<your-domain>")
    registrar_stats()
    

    Contact present + accepted_regs incremented = pass.

  5. Cleanup the test user when done:

    cleanup_test_data(domain="<your-domain>", confirm=True)
    

Test 9 — synthetic INVITE

Same shape but for call setup. If the scenario routes calls through to a back-end (Asterisk, FreeSwitch, carrier trunk):

  1. Provision two test users, or run a UAS-side scenario you control.

  2. Generate a SIPp INVITE scenario:

    sip_generate_sipp_scenario(
        kind="invite_uas_ringing",
        target="<host>:5060",
        from_user="test0001",
        to_user="test0002",
        calls_per_sec=1,
        total_calls=5
    )
    
  3. Run SIPp. While it runs, watch the proxy:

    dlg_list()
    perf_sample_window(duration_sec=10, sample_count=5)
    
  4. Pass criterion: dialogs appear, dialog:active_dialogs ramps up then back down, tm:received_replies shows mostly 200 OKs.


Test 10 — cluster (multi-node only)

cluster_sync_check(cluster_id=1)

healthy: true and every expected node listed with status=OK. If any node shows a non-OK status, do not consider the deploy successful — the cluster is in a degraded state and a single more failure may take the service down. See ha-failover-playbook.md.


Snapshot for the change ticket

After all 10 pass, capture a baseline:

snapshot_capture(out_dir="/var/log/opensips-snapshots", label="post-deploy-vN")

Two reasons:

  • Evidence in the change ticket that the deploy was healthy at sign-off.
  • Baseline for the next deploy's snapshot_diff so you can see what changed between releases.

What to do when something fails

Failed testMost likely causeFirst action
1 healthDaemon didn't start or MI is misconfiguredCheck docker_logs / journalctl; fix config; redeploy
2 sip_options_pingSocket not boundCheck listen= directives + firewall
3 memoryshm under-allocatedcfg_tune with realistic dlg_size_bytes, restart
4 process countConfig syntax error eaten silentlycfg_validate the live config; check log for "WARN: ignoring"
5 dispatcherBackends really down OR ping interval not yet elapsedWait one cycle; if persists, check backend & firewall
6 TLSCert expiringRotate now; tls_reload
7 pikeThreshold too tightTune reqs_density_per_unit
8 REGISTERDB connection broken / auth misconfig / NAT brokenCheck acc:db_* + auth:* log lines + sngrep the REGISTER
9 INVITERouting rule didn't match / dispatcher emptydo_routing log + dispatcher_list_db
10 clusterclusterer / proto_bin not loaded on all nodescluster_sync_check per node; align modparams

If 1, 2, or 3 fail and you can't fix in <2 minutes, roll back:

cfg_rollback(
    backup_path="/etc/opensips/opensips.cfg.bak-<previous-deploy-ts>",
    target_path="/etc/opensips/opensips.cfg",
    validate_after=True
)

Then notify the change-ticket owner. A failed deploy that's rolled back in 5 minutes is recoverable; a failed deploy left to bleed for 30 minutes while you debug is an incident.


Embedding this in CI / CD

If your deploy pipeline can run MCP tool calls (most can via a small shim), make tests 1, 2, 3, 5 mandatory pre-flight gates. Tests 8, 9 need test fixtures and are best run as a separate post-deploy stage that can fail loudly without rolling back the deploy itself.

The full battery takes ~90 seconds; the gating tests take ~10. Both are cheap insurance against deploys that "looked fine" until users noticed.