}
        /// true if an interval boundary was passed by the last call to Update()
        bool Hit() const noexcept {
-               return Running() && mod(value, intv) < last_dt;
+               return Running() && IntervalElapsed() < last_dt;
        }
        bool HitOnce() const noexcept {
                return Running() && value >= intv;
        Time Interval() const noexcept {
                return intv;
        }
+       Time IntervalElapsed() const noexcept {
+               return mod(value, intv);
+       }
+       Time IntervalRemain() const noexcept {
+               return intv - IntervalElapsed();
+       }
        int Iteration() const noexcept {
                return value / intv;
        }
 
        Server(const Config::Network &, World &, const World::Config &, const WorldSave &);
        ~Server();
 
+       // wait for data to arrive for at most dt milliseconds
+       void Wait(int dt) noexcept;
+       // true if there's data waiting to be handled
+       bool Ready() noexcept;
        void Handle();
 
        void Update(int dt);
 private:
        UDPsocket serv_sock;
        UDPpacket serv_pack;
+       SDLNet_SocketSet serv_set;
        std::list<ClientConnection> clients;
 
        World &world;
 
 
 void ServerState::Update(int dt) {
        loop_timer.Update(dt);
+       if (!loop_timer.HitOnce() && loop_timer.IntervalRemain() > 1) {
+               server.Wait(loop_timer.IntervalRemain() - 1);
+               return;
+       }
+       if (dt == 0 && !server.Ready()) {
+               // effectively wait in a spin loop
+               return;
+       }
+
        server.Handle();
        int world_dt = 0;
        while (loop_timer.HitOnce()) {
 
        const WorldSave &save)
 : serv_sock(nullptr)
 , serv_pack{ -1, nullptr, 0 }
+, serv_set(SDLNet_AllocSocketSet(1))
 , clients()
 , world(world)
 , spawn_index(world.Chunks().MakeIndex(wc.spawn, 3))
 , save(save)
 , player_model(nullptr)
 , cli(world) {
+       if (!serv_set) {
+               throw NetError("SDLNet_AllocSocketSet");
+       }
+
        serv_sock = SDLNet_UDP_Open(conf.port);
        if (!serv_sock) {
+               SDLNet_FreeSocketSet(serv_set);
                throw NetError("SDLNet_UDP_Open");
        }
 
+       if (SDLNet_UDP_AddSocket(serv_set, serv_sock) == -1) {
+               SDLNet_UDP_Close(serv_sock);
+               SDLNet_FreeSocketSet(serv_set);
+               throw NetError("SDLNet_UDP_AddSocket");
+       }
+
        serv_pack.data = new Uint8[sizeof(Packet)];
        serv_pack.maxlen = sizeof(Packet);
 }
 
 Server::~Server() {
+       for (ClientConnection &client : clients) {
+               client.Disconnected();
+       }
+       clients.clear();
        world.Chunks().UnregisterIndex(spawn_index);
        delete[] serv_pack.data;
+       SDLNet_UDP_DelSocket(serv_set, serv_sock);
        SDLNet_UDP_Close(serv_sock);
+       SDLNet_FreeSocketSet(serv_set);
 }
 
 
+void Server::Wait(int dt) noexcept {
+       SDLNet_CheckSockets(serv_set, dt);
+}
+
+bool Server::Ready() noexcept {
+       return SDLNet_CheckSockets(serv_set, 0) > 0;
+}
+
 void Server::Handle() {
        int result = SDLNet_UDP_Recv(serv_sock, &serv_pack);
        while (result > 0) {