Performance guide
EzClean is designed to operate with minimal TPS impact even on heavily-loaded servers. All performance-intensive features are disabled by default — enable them when your server’s entity counts justify it.
How cleanup works
- Collect phase — EzClean iterates loaded entities in each enabled world, evaluates each
against the removal rules, and builds a
List<Entity>of candidates. No entities are removed yet.- Paper/Spigot with
async-removal: true: this phase runs on a background thread so the main thread is never blocked by the scan. - Folia: region threading requires the scan to run on the global region thread.
- Paper/Spigot with
- Remove phase — behaviour depends on the server platform:
- Paper/Spigot: entity removal must run on the main thread. Large lists (> 500 entities) are spread across multiple ticks (500 entities/tick) to smooth the spike.
- Folia with
async-removal: true: each entity’sremove()is dispatched to the thread that owns its chunk region via the entity scheduler. All removals execute concurrently across region threads at the next region tick. - Folia with
async-removal: false: entities are removed synchronously on the global region thread.
- Stats phase — duration, entity counts, and TPS samples are recorded. The stats file is written asynchronously so I/O never blocks the main thread.
Async entity removal
By default, all entities are removed in a single tick. On servers with tens of thousands of entities scheduled for removal, this can cause a noticeable TPS spike.
Enabling async removal improves throughput in a platform-specific way:
| Platform | Collect phase | Remove phase |
|---|---|---|
| Paper / Spigot | Runs on a background thread — main thread unblocked during scan | Batched onto the main thread (async-removal-batch-size entities/tick) to comply with Bukkit thread rules |
| Folia | Runs on the global region thread (required by Folia) | Dispatched concurrently to each entity’s owning region thread via the entity scheduler |
On Paper/Spigot the removal phase still runs on the main thread because Bukkit entity operations are not thread-safe. The async scan removes the scan cost from the main thread, and tick-spreading limits per-tick removal cost to ~500 entities. Do not expect true off-thread removal on Paper/Spigot.
On Folia entity removal is genuinely concurrent — each entity.remove() is dispatched
to the region thread that owns that entity’s chunk, so hundreds of entities across different
regions are removed in parallel.
Enabling async removal
Via command (live, no restart needed):
/ezclean toggle async-removal
/ezclean toggle async-removal <cleaner_id> # if you have multiple profiles
Via YAML (edit cleaners/<id>.yml, then /ezclean reload):
performance:
async-removal: true
When to enable it
| Server size | Entities per cleanup | Recommendation |
|---|---|---|
| Small (< 50 players) | < 1 000 | Not needed |
| Medium (50–200 players) | 1 000–5 000 | Consider enabling |
| Large (200+ players) | 5 000+ | Strongly recommended |
Use /ezclean stats to check how many entities were removed in recent runs.
Pile detection
Pile detection scans for clusters of the same entity type at the same block position. This is relatively cheap — O(n) over the entity list — but it does run a full scan during the warning phase to build the pre-clean summary.
After the first cleanup run, the summary is cached from the previous run’s collect phase. The expensive scan is only performed once per server start until the first cleanup fires.
Stats file I/O
Stats are written to EzClean/stats/ after each run. The write happens on an async
Bukkit task (debounced — at most one write queued at a time) so the main thread is
never blocked by file I/O.
Reducing entity counts at the source
EzClean cleans up what is already there — it does not prevent entity spawning. For sustained performance improvements consider:
- Raising
max-entity-cramminginbukkit.yml - Using Paper’s per-world entity limits
- Profiling with Spark to identify the root source of entity buildup
Skipping cleanup on empty servers
When players are offline the cleanup pass still iterates every loaded entity in every enabled world. On servers that unload chunks aggressively this is cheap, but on networks with always-loaded spawn points it can still touch thousands of entities for no player benefit.
Use the minimum player gate to skip the run entirely:
# cleaners/default.yml
min-players: 1 # skip if no players are online
For world-specific savings — for example the nether loads large areas despite few players — set a per-world threshold:
world-overrides:
world_nether:
min-players: 1 # skip nether cleanup when nobody is in the nether
The check is evaluated at cleanup time (not tick time), so the gate adds zero ongoing overhead. If the threshold is not met the entire run is aborted before the entity scan starts.