Files
XTLS_Xray-docs-next/docs/en/development/lua/guide/lifecycle.md
T

4.4 KiB

Instances and Lifecycle

The instance management strategy used by a script entry point determines how Lua instances are created, Hooks are called, state is retained, and instances are destroyed. This page describes these rules by lifecycle type.

Pooled Lifecycle

Currently, HandleRoute in routing scripts and HandleDNSQuery in DNS scripts use pooled instances. The following rules apply to both Hooks.

Instance Pools and Initialization

Routing and DNS scripts are bound through the script field in their respective configurations and manage separate Lua instance pools. Even when configured to use the same file, they do not share state within Lua instances.

At startup, Xray reads and compiles the script once, creates the first instance, executes its top-level code, and checks that the required handler exists. Failure to read the file, parse it, execute top-level code, or validate the handler prevents Xray from starting.

Hook Calls and Instance Recycling

Each routing decision or DNS query exclusively uses one instance. An idle instance is reused when available; when concurrent calls require more instances, new ones are created from the compiled script and the top-level code runs again.

After a handler completes normally, the instance can return to the pool for reuse. An uncaught Lua exception or execution timeout destroys the instance. Reporting a business error through return values, or failing return-value validation, is not a Lua execution exception: the instance can still be reused. Excess idle instances are destroyed automatically.

When an idle instance is available, each Hook follows a lightweight path: take an instance → execute the Hook → return the instance. The script is read and compiled at Xray startup. The green nodes in the diagram show this normal path.

flowchart TD
    LOAD["Xray startup<br/>Read and compile the main script once"] --> INIT["Create the first Lua instance<br/>Run top-level code and check the Hook"]
    INIT --> POOL[("Idle instance pool")]

    subgraph CALL["Pooled Hook call: lightweight reuse path"]
        TAKE["Take exclusive use of an idle instance"] --> RUN["Execute HandleRoute / HandleDNSQuery"]
        RUN -->|Normal completion| PUT["Return the instance, retaining state"]
    end

    REQUEST["Routing decision / DNS query"] --> AVAILABLE{"Idle instance available?"}
    POOL -.-> AVAILABLE
    AVAILABLE -->|Yes| TAKE
    PUT --> POOL
    AVAILABLE -. No: expand on demand .-> CREATE["Create a new instance from the compiled script<br/>Run top-level code and check the Hook"]
    CREATE --> RUN
    RUN -. Uncaught Lua exception / timeout .-> DESTROY["Destroy the instance"]
    POOL -. Excess idle instances / Xray shutdown .-> DESTROY

    classDef reuse fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
    class TAKE,RUN,PUT reuse

State in Pooled Instances

Global variables, top-level local variables, closures, and the require module cache belong to the current instance. They persist across calls handled by that instance and are lost when it is destroyed:

local calls = 0

function HandleRoute()
    calls = calls + 1
    return "direct", "instance-call-" .. calls
end

The counter above only counts calls handled by the current instance. Different instances have independent calls values, and requests are not guaranteed to use the same instance. It cannot serve as a counter shared by all connections or as persistent state for a particular connection.

Place initialization code, such as loading Lua modules, creating matchers, and saving DNS server objects, outside the Hook function at the script's top level. It runs once when each instance is created, and subsequent Hook calls on that instance can reuse the results.

Timeouts and Script Updates

Currently, each routing and DNS instance has an initialization timeout of 120 seconds, and each handler call has an execution timeout of 6 seconds. The call timeout starts after an instance is acquired; these values currently have no separate configuration fields. When calling Xray APIs, cancellation also depends on whether the API responds to cancellation signals; each DNS server is additionally limited by its timeoutMs setting.

Xray does not automatically watch or recompile the main script. Restart Xray after changing it for the changes to take effect. Instances created later also use the main script compiled during the current startup.