nativegate
← Blog

How to deploy a legacy engineering model as an API

Once a legacy model is importable from Python, exposing it over HTTP looks like a small step — and it can be, if you take it with the right respect for what the model actually is. Engineering models are not CRUD applications. This post covers the decisions that matter when you put one behind an API.

First: is your model stateful?

Most legacy Fortran and some C++ is stateful — COMMON blocks, static file-scope variables, or an internal configure-then-read session shape. This determines your concurrency story, and it is not optional to answer.

  • Fortran with COMMON blocks: the state is process-global. Interleaved calls silently corrupt each other's answers. The generated service holds a process-wide lock across each native call — sequential inside a process, scale by running more processes.
  • C++ with file-scope statics: a fresh library instance per request protects request-independent calls, but static state behaves like COMMON: either verify the model is stateless or serialize.
  • Session-shaped models: calls like configure(p, t) followed by calculate() are not safely shared between tenants; one tenant's configuration is another's surprise.

nativegate encodes this in what it generates, rather than asking you to remember it: Fortran services get the lock; the service refuses more than one native call at a time per process. If your capacity math requires removing that lock, the correct answer is more replicas, not less correctness.

What the generated service gives you

The generated service is an ordinary FastAPI app, and the operations-grade parts are generated with it rather than promised for later: API-key authentication, rate limiting, a request-size cap, request IDs and access logging, an exception handler, /healthz and /readyz with SIGTERM draining, a multi-stage non-root container, and Kubernetes manifests that match the container's user. Deploying one looks like:

ship it
$ ngate docker petro_api --build && ngate k8s petro_api

What it deliberately does not give you

  • Per-call isolation. A segfault in native code takes the worker down. There is no sandbox, no per-call timeout. The mitigations are process-level: restarts, replica counts, and not exposing routines to callers who can trigger the crash.
  • Multi-tenancy. These services are for internal, single-tenant deployment — behind your existing boundary, not internet-facing shared infrastructure.
  • Unbounded payloads. Array size caps are a memory guard at the HTTP layer, not a physics-level bounds check.

These are stated plainly because someone will otherwise discover each of them during an incident rather than a design review.

The precondition: verify before you serve

An API multiplies the number of people standing behind the numerics. Which is exactly why the baseline check comes first: record known configurations with ngate golden record, wire ngate golden verify into CI, and only then start serving. The API's job is to widen access to correct answers — the golden record is what makes "correct" a maintained property instead of a one-time audit.

See the generated service → Related: the worked petroleum example