Migration guide · reviewed 22 Aug 2026

From CGI to PSGI without changing behaviour.

Moving a script into a persistent process changes more than startup time. Freeze the HTTP contract first, then make process lifetime, cleanup and deployment explicit.

Independently written12 minProcedure, no copied legacy code

01

Inventory the contract, not the files

A CGI script may read query parameters, form bodies, cookies and server variables, then print headers and a body. The maintenance contract is the observable result: status, headers, body, redirects and side effects for a representative request set.

Do not log secrets to learn the systemRedact authorization headers, cookies, passwords, tokens and uploaded content before requests become fixtures.

02

Build a characterisation seam

Choose ordinary and hostile inputs: missing parameters, repeated parameters, wrong methods, malformed bodies, oversized values and failed writes. Record only behaviour you intend to preserve. If the old response reflects unsanitised input or exposes a file path, the test should describe a defect to remove, not a contract to keep.

Request
Method, path, query, selected headers and a redacted body fixture.
Response
Status, ordered redirects, security-relevant headers and stable body assertions.
Side effect
Files, mail, database writes and external calls, each behind a replaceable boundary.
Failure
What the user sees, what is logged and whether retrying can duplicate work.

03

Audit process-lifetime assumptions

Classic CGI normally starts a process for a request and then exits. A PSGI application can remain loaded and serve later requests. That makes hidden globals, cached user data, changed working directories, stale filehandles and incomplete cleanup observable across requests. The PSGI FAQ calls this out directly: request cleanup that happened accidentally at process exit becomes application work.

  • Reset request-specific state explicitly.
  • Open resources with scoped lifetime and close them on every path.
  • Do not store user or request data in package variables.
  • Assume equivalent application objects may run in multiple processes; a memory flag is not a lock.

04

Choose the smallest migration step

There is no single correct adapter. An application already built on CGI::Application can expose its PSGI coderef. A largely untouched CGI program can be contained temporarily with CGI::Compile and CGI::Emulate::PSGI. New request-handling code can return the PSGI response shape directly or use a framework that supports PSGI.

A wrapper is a checkpointWrapping old code gives you a testable deployment boundary. It does not remove unsafe parsing, global state or filesystem races inside the script.

05

Cut over with two proofs

  1. Run the same redacted request fixtures against the old and candidate paths.
  2. Compare status, redirects, headers, stable body fields and side effects.
  3. Run consecutive requests in one process to expose leaked state.
  4. Exercise concurrent requests where shared files or records are written.
  5. Deploy behind a reversible route or upstream switch.
  6. Watch errors, latency and side-effect counts before removing the old path.

06

Primary sources

Sources checked 22 August 2026. This page describes a migration procedure; it does not claim that a specific application has passed it.