Quick answer: ParticleSystem.Stop() stops emitting by default; live particles finish their lifetime. Use Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear) to remove them instantly. The Stop Action (Disable, Destroy, Callback) runs only once the system has stopped and every particle has died — which never happens for a looping system you never stop.

Two separate features share the word "stop": the Stop() method, which controls emission, and the Main module's Stop Action, which decides what happens to the GameObject afterwards. Most particle clean-up bugs come from expecting one to do the other's job.

Stop() and its behaviours

ps.Stop();   // = Stop(true, ParticleSystemStopBehavior.StopEmitting)

The first argument, withChildren, also stops child systems. Call it on the root of a multi-part effect; stopping only a child leaves the others running.

Stop Action

In the Main module, Stop Action runs after the system is stopped and its last particle has died:

void OnParticleSystemStopped()
{
    pool.Release(gameObject);
}

"My effect is never cleaned up"

"My particles vanish instantly"

You called Stop(true, StopEmittingAndClear), Clear(), or deactivated the GameObject directly with SetActive(false) — which hides all particles at once. To let them fade, call Stop() and let the Disable stop action deactivate the object afterwards.

Pooling pattern

public void Play(Vector3 pos)
{
    transform.position = pos;
    gameObject.SetActive(true);
    ps.Clear(true);
    ps.Play(true);
}
// Main module: Looping off, Stop Action = Disable (or Callback to return to the pool)

Clearing before playing removes particles left over from the previous use.

Checking state

ps.isEmitting tells you whether it is still spawning; ps.IsAlive(true) whether any particles (including children) are still alive; ps.isStopped whether it is fully stopped. Waiting for !ps.IsAlive(true) is the code equivalent of a stop action.

Related fixes