rot.z *= (random.Next<unsigned short>() % 1024);
 
        Entity &e = world.AddEntity();
-       e.Name("spawned");
        e.Position(chunk, pos);
        e.Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } });
        e.WorldCollidable(true);
        Controller *ctrl;
        if (random()) {
                ctrl = new RandomWalk(e, random.Next<std::uint64_t>());
+               e.Name("spawned walker");
        } else {
                ctrl = new Chaser(world, e, reference);
+               e.Name("spawned chaser");
        }
        controllers.emplace_back(ctrl);
 }
 
 }
 
 void Chaser::Update(int dt) {
+       if (Target().Dead()) {
+               Controlled().Kill();
+               return;
+       }
+
        glm::vec3 diff(Target().AbsoluteDifference(Controlled()));
        float dist = length(diff);
        if (dist < std::numeric_limits<float>::epsilon()) {
 
 #include "../net/Client.hpp"
 #include "../net/ConnectionHandler.hpp"
 
+#include <map>
 #include <memory>
 
 
        void On(const Packet::DespawnEntity &) override;
        void On(const Packet::EntityUpdate &) override;
 
+private:
+       /// flag entity as updated by given packet
+       /// returns false if the update should be ignored
+       bool UpdateEntity(std::uint32_t id, std::uint16_t seq);
+       /// drop update information or given entity
+       void ClearEntity(std::uint32_t id);
+
 private:
        Environment &env;
        World::Config world_conf;
 
        int login_packet;
 
+       struct UpdateStatus {
+               std::uint16_t last_packet;
+               int last_update;
+       };
+       std::map<std::uint32_t, UpdateStatus> update_status;
+       IntervalTimer update_timer;
+
 };
 
 }
 
 , state()
 , client(cc)
 , init_state(*this)
-, login_packet(-1) {
+, login_packet(-1)
+, update_status()
+, update_timer(16) {
        client.GetConnection().SetHandler(this);
+       update_timer.Start();
 }
 
 void MasterState::Quit() {
 
 
 void MasterState::Update(int dt) {
+       update_timer.Update(dt);
        client.Handle();
        client.Update(dt);
 }
        } else {
                // joining game
                cout << "joined game \"" << world_conf.name << '"' << endl;
+               // server received our login
+               login_packet = -1;
        }
 
        uint32_t player_id;
        }
        uint32_t entity_id;
        pack.ReadEntityID(entity_id);
-       Entity *entity = state->GetWorld().AddEntity(entity_id);
-       if (!entity) {
-               cout << "entity ID inconsistency" << endl;
-               Quit();
-               return;
-       }
-       pack.ReadEntity(*entity);
+       Entity &entity = state->GetWorld().ForceAddEntity(entity_id);
+       UpdateEntity(entity_id, pack.Seq());
+       pack.ReadEntity(entity);
        uint32_t skel_id;
        pack.ReadSkeletonID(skel_id);
        CompositeModel *skel = state->GetSkeletons().ByID(skel_id);
        if (skel) {
-               skel->Instantiate(entity->GetModel());
+               skel->Instantiate(entity.GetModel());
        }
-       cout << "spawned entity " << entity->Name() << " at " << entity->AbsolutePosition() << endl;
+       cout << "spawned entity " << entity.Name() << " at " << entity.AbsolutePosition() << endl;
 }
 
 void MasterState::On(const Packet::DespawnEntity &pack) {
        }
        uint32_t entity_id;
        pack.ReadEntityID(entity_id);
+       ClearEntity(entity_id);
        for (Entity &entity : state->GetWorld().Entities()) {
                if (entity.ID() == entity_id) {
                        entity.Kill();
                        return;
                }
                if (world_iter->ID() == entity_id) {
-                       pack.ReadEntity(*world_iter, i);
+                       if (UpdateEntity(entity_id, pack.Seq())) {
+                               pack.ReadEntity(*world_iter, i);
+                       }
                }
        }
 }
 
+bool MasterState::UpdateEntity(uint32_t entity_id, uint16_t seq) {
+       auto entry = update_status.find(entity_id);
+       if (entry == update_status.end()) {
+               update_status.emplace(entity_id, UpdateStatus{ seq, update_timer.Elapsed() });
+               return true;
+       }
+
+       int pack_diff = int16_t(seq) - int16_t(entry->second.last_packet);
+       int time_diff = update_timer.Elapsed() - entry->second.last_update;
+       entry->second.last_update = update_timer.Elapsed();
+
+       if (pack_diff > 0 || time_diff > 1500) {
+               entry->second.last_packet = seq;
+               return true;
+       } else {
+               return false;
+       }
+}
+
+void MasterState::ClearEntity(uint32_t entity_id) {
+       update_status.erase(entity_id);
+}
+
 }
 }
 
 #define BLANK_NET_CLIENT_HPP_
 
 #include "Connection.hpp"
+#include "../app/IntervalTimer.hpp"
 
 #include <string>
 #include <SDL_net.h>
        std::uint16_t SendPing();
        std::uint16_t SendLogin(const std::string &);
        std::uint16_t SendPart();
-       std::uint16_t SendPlayerUpdate(const Entity &);
+       // this may not send the update at all, in which case it returns -1
+       int SendPlayerUpdate(const Entity &);
 
 private:
        void HandlePacket(const UDPpacket &);
        Connection conn;
        UDPsocket client_sock;
        UDPpacket client_pack;
+       IntervalTimer update_timer;
 
 };
 
 
 
 #include "Connection.hpp"
 #include "ConnectionHandler.hpp"
+#include "../app/IntervalTimer.hpp"
 
 #include <list>
 #include <SDL_net.h>
        Entity *player;
        std::list<SpawnStatus> spawns;
        unsigned int confirm_wait;
+       std::uint16_t player_update_pack;
+       IntervalTimer player_update_timer;
 
 };
 
 
                std::size_t length;
                std::uint8_t *data;
 
+               std::uint16_t Seq() const noexcept {
+                       return reinterpret_cast<const Packet *>(data - sizeof(Header))->header.ctrl.seq;
+               }
+
                template<class T>
                void Write(const T &, size_t off) noexcept;
                template<class T>
 
 Client::Client(const Config &conf)
 : conn(client_resolve(conf.host.c_str(), conf.port))
 , client_sock(client_bind(0))
-, client_pack{ -1, nullptr, 0 } {
+, client_pack{ -1, nullptr, 0 }
+, update_timer(16) {
        client_pack.data = new Uint8[sizeof(Packet)];
        client_pack.maxlen = sizeof(Packet);
        // establish connection
        SendPing();
+       update_timer.Start();
 }
 
 Client::~Client() {
 }
 
 void Client::Update(int dt) {
+       update_timer.Update(dt);
        conn.Update(dt);
        if (conn.ShouldPing()) {
                SendPing();
        return conn.Send(client_pack, client_sock);
 }
 
-uint16_t Client::SendPlayerUpdate(const Entity &player) {
+int Client::SendPlayerUpdate(const Entity &player) {
+       // don't send all too many updates
+       if (!update_timer.Hit()) return -1;
        auto pack = Packet::Make<Packet::PlayerUpdate>(client_pack);
        pack.WritePlayer(player);
        return conn.Send(client_pack, client_sock);
 , conn(addr)
 , player(nullptr)
 , spawns()
-, confirm_wait(0) {
+, confirm_wait(0)
+, player_update_pack(0)
+, player_update_timer(1500) {
        conn.SetHandler(this);
 }
 
                response.WritePlayer(*new_player);
                response.WriteWorldName(server.GetWorld().Name());
                conn.Send(server.GetPacket(), server.GetSocket());
+               // set up update tracking
+               player_update_pack = pack.Seq();
+               player_update_timer.Reset();
+               player_update_timer.Start();
        } else {
                // aw no :(
                cout << "rejected login from player \"" << name << '"' << endl;
 
 void ClientConnection::On(const Packet::PlayerUpdate &pack) {
        if (!HasPlayer()) return;
-       pack.ReadPlayer(Player());
+       int pack_diff = int16_t(pack.Seq()) - int16_t(player_update_pack);
+       bool overdue = player_update_timer.HitOnce();
+       player_update_timer.Reset();
+       if (pack_diff > 0 || overdue) {
+               player_update_pack = pack.Seq();
+               pack.ReadPlayer(Player());
+       }
 }
 
 
 
        return &*entity;
 }
 
+Entity &World::ForceAddEntity(std::uint32_t id) {
+       if (entities.empty() || entities.back().ID() < id) {
+               entities.emplace_back();
+               entities.back().ID(id);
+               return entities.back();
+       }
+
+       auto position = entities.begin();
+       auto end = entities.end();
+       while (position != end && position->ID() < id) {
+               ++position;
+       }
+       if (position != end && position->ID() == id) {
+               return *position;
+       }
+       auto entity = entities.emplace(position);
+       entity->ID(id);
+       return *entity;
+}
+
 
 namespace {
 
 
        /// add entity with given ID
        /// returns nullptr if the ID is already taken
        Entity *AddEntity(std::uint32_t id);
+       /// add entity with given ID
+       /// returs an existing entity if ID is already taken
+       Entity &ForceAddEntity(std::uint32_t id);
 
        const std::vector<Player> &Players() const noexcept { return players; }
        std::list<Entity> &Entities() noexcept { return entities; }