diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPreShutdownEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPreShutdownEvent.java
new file mode 100644
index 00000000..893942c0
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPreShutdownEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018-2025 Velocity Contributors
+ *
+ * The Velocity API is licensed under the terms of the MIT License. For more details,
+ * reference the LICENSE file in the api top-level directory.
+ */
+
+package com.velocitypowered.api.event.proxy;
+
+import com.google.common.annotations.Beta;
+import com.velocitypowered.api.event.annotation.AwaitingEvent;
+
+/**
+ * This event is fired by the proxy after it has stopped accepting new connections,
+ * but before players are disconnected.
+ * This is the last point at which you can interact with currently connected players,
+ * for example to transfer them to another proxy or perform other cleanup tasks.
+ *
+ * @implNote Velocity will wait for all event listeners to complete before disconnecting players,
+ * but note that the event will time out after the configured value of the
+ * velocity.pre-shutdown-timeout system property, default 10 seconds,
+ * in seconds to prevent shutdown from hanging indefinitely
+ * @since 3.4.0
+ */
+@Beta
+@AwaitingEvent
+public final class ProxyPreShutdownEvent {
+
+ @Override
+ public String toString() {
+ return "ProxyPreShutdownEvent";
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index 7d55ae23..ccfc5f14 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -24,6 +24,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
+import com.velocitypowered.api.event.proxy.ProxyPreShutdownEvent;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.network.ProtocolVersion;
@@ -150,6 +151,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
)
.registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE)
.create();
+ private static final int PRE_SHUTDOWN_TIMEOUT =
+ Integer.getInteger("velocity.pre-shutdown-timeout", 10);
private final ConnectionManager cm;
private final ProxyOptions options;
@@ -579,6 +582,20 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
// done first to refuse new connections
cm.shutdown();
+ try {
+ eventManager.fire(new ProxyPreShutdownEvent())
+ .toCompletableFuture()
+ .get(PRE_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
+ } catch (TimeoutException ignored) {
+ logger.warn("Your plugins took over {} seconds during pre shutdown.",
+ PRE_SHUTDOWN_TIMEOUT);
+ } catch (ExecutionException ee) {
+ logger.error("Exception in ProxyPreShutdownEvent handler; continuing shutdown.", ee);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ logger.warn("Interrupted while waiting for ProxyPreShutdownEvent; continuing shutdown.");
+ }
+
ImmutableList players = ImmutableList.copyOf(connectionsByUuid.values());
for (ConnectedPlayer player : players) {
player.disconnect(reason);