diff --git a/.env.defaults b/.env.defaults
index 0131a288d98357fad451d8e9d3626f59137212c4..cf8181fd24fea5a3368189d2b1a622d162b3d3da 100644
--- a/.env.defaults
+++ b/.env.defaults
@@ -2,6 +2,7 @@ NODE_ENV=production
 CACHE_TTL=30000
 PORT=8080
 METRICS_PORT=9090
+LIGHTSHIP_PORT=9000
 LOG_LEVEL=info
 APP_ROOT=/
 EXPOSE_API_DOCS=false
diff --git a/helm/core-ui-middleware/templates/deployment.yaml b/helm/core-ui-middleware/templates/deployment.yaml
index c9d328abe1f3e560a20eadbfb938b021189421c4..422818d193d468a1d669bd28e6acc20933a61f5d 100644
--- a/helm/core-ui-middleware/templates/deployment.yaml
+++ b/helm/core-ui-middleware/templates/deployment.yaml
@@ -86,6 +86,43 @@ spec:
               memory: 96Mi
             requests:
               memory: 96Mi
+        - name: updater
+          securityContext: {{ toYaml .Values.securityContext | nindent 12 }}
+          image: {{ include "ox-common.images.image" (dict "imageRoot" .Values.image "global" $ "context" . ) }}
+          command: ["/nodejs/bin/node", "src/updater.js"]
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          env:
+            - name: CACHE_TTL
+              value: "{{ .Values.cacheTTL | int }}"
+            - name: LOG_LEVEL
+              value: "{{ .Values.logLevel }}"
+            - name: APP_ROOT
+              value: "{{ include "ox-common.appsuite.appRoot" . }}/"
+            - name: COMPRESS_FILE_SIZE
+              value: "{{ .Values.compressFileSize }}"
+            - name: COMPRESS_FILE_TYPES
+              value: "{{ .Values.compressFileTypes }}"
+            - name: SLOW_REQUEST_THRESHOLD
+              value: "{{ .Values.slowRequestThreshold }}"
+            - name: METRICS_PORT
+              value: "9091"
+            - name: LIGHTSHIP_PORT
+              value: "9001"
+            - name: PORT
+              value: "8081"
+            - name: REDIS_MODE
+              value: "{{ .Values.redis.mode }}"
+            - name: REDIS_HOSTS
+              value: "{{ .Values.redis.hosts | join "," }}"
+            - name: REDIS_DB
+              value: "{{ .Values.redis.db | int }}"
+            - name: REDIS_PASSWORD
+              value: "{{ .Values.redis.password }}"
+            - name: REDIS_PREFIX
+              value: "{{ .Values.redis.prefix }}"
+          volumeMounts:
+            - name: manifest-config
+              mountPath: /app/config/
         {{- end }}
       volumes:
         - name: manifest-config
diff --git a/helm/core-ui-middleware/templates/updater.yaml b/helm/core-ui-middleware/templates/updater.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..dab7502ee90853aa8bc79d981fa290bce715a6fd
--- /dev/null
+++ b/helm/core-ui-middleware/templates/updater.yaml
@@ -0,0 +1,75 @@
+{{- if not (and (eq (len .Values.redis.hosts) 1) (eq (index .Values.redis.hosts 0) "localhost:6379")) }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "ox-common.names.fullname" . }}-updater
+  labels:
+    {{- include "ox-common.labels.standard" . | nindent 4 }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      {{- include "ox-common.labels.matchLabels" . | nindent 6 }}
+  template:
+    metadata:
+      annotations: {{ toYaml .Values.podAnnotations | nindent 8 }}
+      labels:
+        {{- include "ox-common.labels.podLabels" . | nindent 8 }}
+    spec: {{ include "ox-common.pods.podSpec" (dict "podRoot" .Values "global" $ "context" . ) | nindent 6 }}
+      containers:
+        - name: main
+          securityContext: {{ toYaml .Values.securityContext | nindent 12 }}
+          image: {{ include "ox-common.images.image" (dict "imageRoot" .Values.image "global" $ "context" . ) }}
+          command: ["/nodejs/bin/node", "src/updater.js"]
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          env:
+            - name: CACHE_TTL
+              value: "{{ .Values.cacheTTL | int }}"
+            - name: LOG_LEVEL
+              value: "{{ .Values.logLevel }}"
+            - name: APP_ROOT
+              value: "{{ include "ox-common.appsuite.appRoot" . }}/"
+            - name: COMPRESS_FILE_SIZE
+              value: "{{ .Values.compressFileSize }}"
+            - name: COMPRESS_FILE_TYPES
+              value: "{{ .Values.compressFileTypes }}"
+            - name: SLOW_REQUEST_THRESHOLD
+              value: "{{ .Values.slowRequestThreshold }}"
+            - name: REDIS_MODE
+              value: "{{ .Values.redis.mode }}"
+            - name: REDIS_HOSTS
+              value: "{{ .Values.redis.hosts | join "," }}"
+            - name: REDIS_DB
+              value: "{{ .Values.redis.db | int }}"
+            - name: REDIS_PASSWORD
+              value: "{{ .Values.redis.password }}"
+            - name: REDIS_PREFIX
+              value: "{{ .Values.redis.prefix }}"
+          ports:
+            - name: tcp-monitoring
+              containerPort: 9090
+              protocol: TCP
+          {{- if .Values.probe.liveness.enabled }}
+          livenessProbe:
+            httpGet:
+              path: /live
+              port: 9000
+            {{- omit .Values.probe.liveness "enabled" | toYaml | nindent 12 }}
+          {{- end }}
+          {{- if .Values.probe.readiness.enabled }}
+          readinessProbe:
+            httpGet:
+                path: /ready
+                port: 9000
+            {{- omit .Values.probe.readiness "enabled" | toYaml | nindent 12 }}
+          {{- end }}
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          volumeMounts:
+            - name: manifest-config
+              mountPath: /app/config/
+      volumes:
+        - name: manifest-config
+          configMap:
+            name: {{ .Values.existingConfigMap | default (include "ox-common.names.fullname" .) }}
+{{- end }}
diff --git a/integration/caching_test.js b/integration/caching_test.js
index 8c88ce94612ec679a0f9c6857499731843548a74..2ee477b2fadd7f826398852d080b19a5c9b303a7 100644
--- a/integration/caching_test.js
+++ b/integration/caching_test.js
@@ -27,7 +27,7 @@ import { getRedisKey } from '../src/util.js'
 import zlib from 'node:zlib'
 
 describe('File caching service', function () {
-  let app
+  let app, pubClient
 
   beforeEach(async function () {
     await mockConfig({ baseUrls: ['http://ui-server/'] })
@@ -39,7 +39,14 @@ describe('File caching service', function () {
         '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } })
       }
     })
-    await import('../src/redis.js').then(({ client }) => client.flushdb())
+    await import('../src/redis.js').then(({ client, createClient }) => {
+      pubClient = createClient()
+      return client.flushdb()
+    })
+    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
+      await updateVersionProcessor(pubClient)
+      await updateVersionProcessor(pubClient)
+    })
     app = await injectApp()
   })
 
diff --git a/integration/config_test.js b/integration/config_test.js
index 497bca206380a2b6a8e672c217bc9d84aa557845..66fb7dc5d38c266120a9f68ecbe33e0a384d9ba9 100644
--- a/integration/config_test.js
+++ b/integration/config_test.js
@@ -21,13 +21,13 @@
  */
 
 import { expect } from 'chai'
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, wait } from '../spec/util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch } from '../spec/util.js'
 import * as td from 'testdouble'
-import { getRedisKey } from '../src/util.js'
 
 describe('Configuration', function () {
   let app
   let config
+  let pubClient
 
   beforeEach(async function () {
     // need to set the redis-prefix. Otherwise, the bull workers will interfere
@@ -40,20 +40,23 @@ describe('Configuration', function () {
         }),
         '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
         '/meta.json': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn(
+          new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }),
           new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }),
           new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } })
         )
       }
     })
-    await import('../src/redis.js').then(({ client }) => client.flushdb())
+    await import('../src/redis.js').then(({ client, createClient }) => {
+      pubClient = createClient()
+      return client.flushdb()
+    })
+    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
+      await updateVersionProcessor(pubClient)
+    })
     app = await injectApp()
   })
 
   afterEach(async function () {
-    const { getQueues, closeQueue } = await import('../src/redis.js')
-    for (const queue of getQueues()) {
-      await closeQueue(queue.name)
-    }
     td.reset()
     // reset, after the queues were removed
     process.env.REDIS_PREFIX = 'ui-middleware'
@@ -64,9 +67,10 @@ describe('Configuration', function () {
     expect(response.json()).to.have.length(2)
 
     config.baseUrls = []
-    const { pubClient } = await import('../src/redis.js')
-    pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' }))
-    await wait(200)
+    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
+      await updateVersionProcessor(pubClient)
+      await updateVersionProcessor(pubClient)
+    })
 
     const response2 = await app.inject({ url: '/meta' })
     expect(response2.json()).to.have.length(1)
diff --git a/integration/update-version_test.js b/integration/update-version_test.js
index 06bd36b9c0417fe1f6666fa5a9923c87774a6b24..949df58717ac414bae0288686bf486b7c94a46f5 100644
--- a/integration/update-version_test.js
+++ b/integration/update-version_test.js
@@ -21,12 +21,14 @@
  */
 
 import { expect } from 'chai'
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, wait } from '../spec/util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch } from '../spec/util.js'
 import * as td from 'testdouble'
 import { getRedisKey } from '../src/util.js'
 
 describe('Updates the version', function () {
   let app
+  let pubClient
+  let runUpdate
 
   beforeEach(async function () {
     // need to set the redis-prefix. Otherwise, the bull workers will interfere
@@ -40,20 +42,27 @@ describe('Updates the version', function () {
         '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
         '/meta.json': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn(
           new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }),
+          new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }),
+          new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }),
+          new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } }),
           new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } }),
           new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } })
         )
       }
     })
-    await import('../src/redis.js').then(({ client }) => client.flushdb())
+    await import('../src/redis.js').then(({ client, createClient }) => {
+      pubClient = createClient('pubClient')
+      return client.flushdb()
+    })
+    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
+      runUpdate = updateVersionProcessor
+      await runUpdate(pubClient)
+      return runUpdate(pubClient)
+    })
     app = await injectApp()
   })
 
   afterEach(async function () {
-    const { getQueues, closeQueue } = await import('../src/redis.js')
-    for (const queue of getQueues()) {
-      await closeQueue(queue.name)
-    }
     td.reset()
     // reset, after the queues were removed
     process.env.REDIS_PREFIX = 'ui-middleware'
@@ -64,30 +73,19 @@ describe('Updates the version', function () {
     expect(responseBeforeUpdate.statusCode).to.equal(200)
     expect(responseBeforeUpdate.headers.version).to.equal('85101541')
 
-    const { getQueue } = await import('../src/redis.js')
-    // update has only been registered but not executed yet
-    expect(await getQueue('update-version').add({}).then(job => job.finished())).to.equal('85101541')
-    // update is executed with the second iteration
-    expect(await getQueue('update-version').add({}).then(job => job.finished())).to.equal('85102502')
-
+    await runUpdate(pubClient)
+    await runUpdate(pubClient)
     const responseAfterUpdate = await app.inject({ url: '/index.html' })
     expect(responseAfterUpdate.statusCode).to.equal(200)
     expect(responseAfterUpdate.headers.version).to.equal('85102502')
   })
 
   it('with automatically triggered job', async function () {
+    const subClient = await import('../src/redis.js').then(({ createClient }) => createClient('subClient'))
     const responseBeforeUpdate = await app.inject({ url: '/index.html' })
     expect(responseBeforeUpdate.statusCode).to.equal(200)
     expect(responseBeforeUpdate.headers.version).to.equal('85101541')
 
-    // speed up the update process
-    const { subClient, getQueue } = await import('../src/redis.js')
-    const queue = getQueue('update-version')
-    queue.add({}, {
-      jobId: 'update-version-job',
-      repeat: { every: 100 }
-    })
-
     // wait for the update event to happen
     await new Promise(resolve => {
       const key = getRedisKey({ name: 'updateVersionInfo' })
@@ -96,6 +94,7 @@ describe('Updates the version', function () {
         if (channel !== key) return
         resolve()
       })
+      runUpdate(pubClient).then(() => runUpdate(pubClient))
     })
 
     const responseAfterUpdate = await app.inject({ url: '/index.html' })
@@ -103,21 +102,6 @@ describe('Updates the version', function () {
     expect(responseAfterUpdate.headers.version).to.equal('85102502')
   })
 
-  it('receives version update via redis event', async function () {
-    const responseBeforeUpdate = await app.inject({ url: '/index.html' })
-    expect(responseBeforeUpdate.statusCode).to.equal(200)
-    expect(responseBeforeUpdate.headers.version).to.equal('85101541')
-
-    const { pubClient } = await import('../src/redis.js')
-    // just publish event, don't change the value on redis.
-    pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' }))
-    await wait(10)
-
-    const responseAfterUpdate = await app.inject({ url: '/index.html' })
-    expect(responseAfterUpdate.statusCode).to.equal(200)
-    expect(responseAfterUpdate.headers.version).to.equal('1234')
-  })
-
   describe('with initial version', function () {
     beforeEach(async function () {
       td.reset()
@@ -140,7 +124,11 @@ describe('Updates the version', function () {
       const { client } = await import('../src/redis.js')
       await client.flushdb()
       // preconfigure redis
-      await client.set(getRedisKey({ name: 'versionInfo' }), JSON.stringify({ version: '12345' }))
+      await Promise.all([
+        client.set(getRedisKey({ name: 'versionInfo' }), JSON.stringify({ version: '12345' })),
+        client.set(getRedisKey({ version: '12345', name: '/index.html' }) + ':body', '<html><head></head><body>it\'s me</body></html>'),
+        client.set(getRedisKey({ version: '12345', name: '/index.html' }) + ':meta', JSON.stringify({ headers: { 'content-type': 'text/html', version: '12345' } }))
+      ])
       app = await injectApp()
     })
 
diff --git a/spec/app_root_test.js b/spec/app_root_test.js
index 2dbeb679d23ba5c95460b4f22d606de6336ba636..80bd741d7b5e2f6bae9e7780624e7786fcebc57f 100644
--- a/spec/app_root_test.js
+++ b/spec/app_root_test.js
@@ -20,7 +20,7 @@
  *
  */
 
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { injectApp, mockRedis, mockFetch, mockConfig } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -29,38 +29,22 @@ describe('With different app root', function () {
   let app
 
   beforeEach(async function () {
-    let count = 0
     await mockConfig({ baseUrls: ['http://ui-server/'] })
-    await mockRedis()
+    const { client } = await mockRedis()
     mockFetch({
       'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': { imports: ['test.txt'] },
-          'test.txt': { },
-          'main.css': {},
-          'index.html': {
-            file: 'index.html.js',
-            isEntry: true,
-            imports: ['example.js'],
-            css: ['main.css']
-          },
-          'image.png': {}
-        }),
-        '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
-        '/test.txt': () => new Response('this is test', { headers: { 'content-type': 'text/plain' } }),
-        '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }),
-        '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
-        '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }),
-        '/favicon.ico': 'not really a favicon, though',
-        '/test.svg': () => {
-          if (count > 0) {
-            return new Response(null, { status: 404 })
-          }
-          count++
-          return new Response('<svg></svg>', { headers: { 'content-type': 'image/svg' } })
-        }
+        '/favicon.ico': 'not really a favicon, though'
       }
     })
+    await Promise.all([
+      client.set('ui-middleware:viteManifests', JSON.stringify({})),
+      client.set('ui-middleware:/example.js:body', 'this is example'),
+      client.set('ui-middleware:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })),
+      client.set('ui-middleware:/test.txt:body', 'this is test'),
+      client.set('ui-middleware:/test.txt:meta', JSON.stringify({ headers: { 'content-type': 'text/plain' } })),
+      client.set('ui-middleware:/index.html:body', '<html><head></head><body>it\'s me</body></html>'),
+      client.set('ui-middleware:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } }))
+    ])
     app = await injectApp('/appsuite/')
   })
 
@@ -73,7 +57,7 @@ describe('With different app root', function () {
   it('serves files defined in manifest.json file', async function () {
     const response = await app.inject({ url: '/appsuite/example.js' })
     expect(response.statusCode).to.equal(200)
-    expect(response.headers['content-type']).to.equal('application/javascript; charset=utf-8')
+    expect(response.headers['content-type']).to.equal('application/javascript')
     expect(response.body).to.equal('this is example')
 
     const response2 = await app.inject({ url: '/appsuite/test.txt' })
diff --git a/spec/file-depencies_test.js b/spec/file-depencies_test.js
index 10a0a46a17d5ba147c7c804bcd3b25b76d294425..12c0a93a2443e80d489db2502ba92937c2992365 100644
--- a/spec/file-depencies_test.js
+++ b/spec/file-depencies_test.js
@@ -20,36 +20,26 @@
  *
  */
 
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { injectApp, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
 
 describe('JS files with dependencies contain events', function () {
   let app
-  let mockFetchConfig
+  let client
 
   beforeEach(async function () {
-    await mockConfig({ baseUrls: ['http://ui-server/'] })
-    await mockRedis()
-    mockFetch(mockFetchConfig = {
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': {},
-          'main.css': {},
-          'index.html': {
-            file: 'index.html.js',
-            isEntry: true,
-            imports: ['example.js'],
-            css: ['main.css']
-          }
-        }),
-        '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
-        '/index.html.js': () => new Response('console.log("this is index.html.js")', { headers: { 'content-type': 'application/javascript' } }),
-        '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
-        '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } })
-      }
-    })
+    client = (await mockRedis()).client
+    client.set('ui-middleware:versionInfo', JSON.stringify({ version: '123456' }))
+    client.set('ui-middleware:123456:/example.js:body', 'this is example')
+    client.set('ui-middleware:123456:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } }))
+    client.set('ui-middleware:123456:/index.html.js:body', 'console.log("this is index.html.js")')
+    client.set('ui-middleware:123456:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', dependencies: ['main.css'] } }))
+    client.set('ui-middleware:123456:/index.html:body', '<html><head></head><body>it\'s me</body></html>')
+    client.set('ui-middleware:123456:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } }))
+    client.set('ui-middleware:123456:/main.css:body', '.foo { color: #000; }')
+    client.set('ui-middleware:123456:/main.css:meta', JSON.stringify({ headers: { 'content-type': 'text/css' } }))
     app = await injectApp()
   })
 
@@ -63,26 +53,15 @@ describe('JS files with dependencies contain events', function () {
     const r1 = await app.inject({ url: '/index.html.js' })
     expect(r1.headers.dependencies[0]).to.equal('main.css')
 
-    mockFetchConfig['http://ui-server']['/manifest.json'] = generateSimpleViteManifest({
-      'example.js': {},
-      'other.css': {},
-      'index.html': {
-        file: 'index.html.js',
-        isEntry: true,
-        imports: ['example.js'],
-        css: ['other.css']
-      }
-    })
+    // client.set('ui-middleware:versionInfo', JSON.stringify({ version: '123457' }))
+    client.set('ui-middleware:123457:/index.html.js:body', 'console.log("this is index.html.js")')
+    client.set('ui-middleware:123457:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', dependencies: ['other.css'] } }))
 
-    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
-      // need to process two times to actually trigger the update
-      await updateVersionProcessor()
-      await updateVersionProcessor()
-    })
-
-    const r2 = await app.inject({ url: '/index.html.js' })
+    const r2 = await app.inject({ url: '/index.html.js', headers: { version: '123457' } })
     expect(r2.headers.dependencies[0]).to.equal('other.css')
 
+    console.log('r1.headers.version', r1.headers.version)
+    console.log('r2.headers.version', r2.headers.version)
     const r3 = await app.inject({
       url: '/index.html.js',
       headers: { version: r1.headers.version }
@@ -94,8 +73,5 @@ describe('JS files with dependencies contain events', function () {
       headers: { version: r2.headers.version }
     })
     expect(r4.headers.dependencies[0]).to.equal('other.css')
-
-    const r5 = await app.inject({ url: '/index.html.js' })
-    expect(r5.headers.dependencies[0]).to.equal('other.css')
   })
 })
diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js
index ad4860344b7600353ff4da98c352f8d1d304440b..ec9668e8e90250aa6c955236cb7a314e0eacd41e 100644
--- a/spec/file_caching_test.js
+++ b/spec/file_caching_test.js
@@ -20,7 +20,7 @@
  *
  */
 
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis, wait } from './util.js'
+import { injectApp, mockRedis, mockConfig, mockFetch } from './util.js'
 import fs from 'fs'
 import { expect } from 'chai'
 import * as td from 'testdouble'
@@ -37,46 +37,27 @@ describe('File caching service', function () {
   let redis
 
   beforeEach(async function () {
-    let count = 0
     await mockConfig({ baseUrls: ['http://ui-server/'] })
     redis = await mockRedis()
     mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': { imports: ['test.txt'] },
-          'test.txt': { },
-          'main.css': {},
-          'index.html': {
-            file: 'index.html.js',
-            isEntry: true,
-            imports: ['example.js'],
-            css: ['main.css']
-          },
-          'image.png': {}
-        }),
-        '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
-        '/test.txt': () => new Response('this is test', { headers: { 'content-type': 'text/plain' } }),
-        '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }),
-        '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
-        '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }),
-        '/favicon.ico': 'not really a favicon, though',
-        '/test.svg': () => {
-          if (count > 0) {
-            return new Response(null, { status: 404 })
-          }
-          count++
-          return new Response('<svg></svg>', { headers: { 'content-type': 'image/svg' } })
-        },
-        '/image.png': () => {
-          return new Response(image, {
-            headers: {
-              'Content-Type': 'image/png',
-              'Content-Length': imageStat.size.toString()
-            }
-          })
-        }
-      }
+      'http://ui-server': {}
     })
+    await Promise.all([
+      redis.client.set('ui-middleware:versionInfo', JSON.stringify({ version: 554855300 })),
+      redis.client.set('ui-middleware:554855300:viteManifests', JSON.stringify({})),
+      redis.client.set('ui-middleware:554855300:oxManifests:body', '{}'),
+      redis.client.set('ui-middleware:554855300:oxManifests:meta', JSON.stringify({ headers: { 'content-type': 'application/json' } })),
+      redis.client.set('ui-middleware:554855300:/example.js:body', 'this is example'),
+      redis.client.set('ui-middleware:554855300:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })),
+      redis.client.set('ui-middleware:554855300:/test.txt:body', 'this is test'),
+      redis.client.set('ui-middleware:554855300:/test.txt:meta', JSON.stringify({ headers: { 'content-type': 'text/plain' } })),
+      redis.client.set('ui-middleware:554855300:/index.html:body', '<html><head></head><body>it\'s me</body></html>'),
+      redis.client.set('ui-middleware:554855300:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } })),
+      redis.client.set('ui-middleware:554855300:/main.css:body', 'body { color: red; }'),
+      redis.client.set('ui-middleware:554855300:/main.css:meta', JSON.stringify({ headers: { 'content-type': 'text/css' } })),
+      redis.client.set('ui-middleware:554855300:/image.png:body', image),
+      redis.client.set('ui-middleware:554855300:/image.png:meta', JSON.stringify({ headers: { 'content-type': 'image/png' } }))
+    ])
     app = await injectApp()
   })
 
@@ -88,7 +69,7 @@ describe('File caching service', function () {
   it('serves files defined in manifest.json file', async function () {
     const response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
-    expect(response.headers['content-type']).to.equal('application/javascript; charset=utf-8')
+    expect(response.headers['content-type']).to.equal('application/javascript')
     expect(response.body).to.equal('this is example')
     // expect(response.headers['content-security-policy']).to.contain('sha256-NzZhMTE2Njc2YTgyNTZmZTdlZGVjZDU3YTNmYzRjNmM1OWZkMTI2NjRkYzZmMWM3YTkwMGU3ZTdhNDlhZmVlMwo=')
     const response2 = await app.inject({ url: '/test.txt' })
@@ -111,12 +92,6 @@ describe('File caching service', function () {
     expect(response.body).to.equal('<html><head></head><body>it\'s me</body></html>')
   })
 
-  it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () {
-    const response = await app.inject({ url: '/favicon.ico' })
-    expect(response.statusCode).to.equal(200)
-    expect(response.body).to.equal('not really a favicon, though')
-  })
-
   it('returns 404 if file can not be resolved', async function () {
     const response = await app.inject({ url: '/unknown-file.txt' })
     expect(response.statusCode).to.equal(404)
@@ -130,142 +105,34 @@ describe('File caching service', function () {
   })
 
   it('only fetches files once', async function () {
-    let spy
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/example.js': spy = sandbox.spy(() => {
-          return new Response('this is example', { headers: { 'content-type': 'application/javascript; charset=utf-8' } })
-        })
-      }
-    })
-    app = await injectApp()
-
-    expect(spy.callCount).to.equal(0)
-    let response = await app.inject({ url: '/example.js' })
-    expect(response.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
-    response = await app.inject({ url: '/example.js' })
-    expect(response.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
-  })
-
-  it('only fetches files once, but deliver from local cache', async function () {
-    let spy
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/example.js': spy = sandbox.spy(() => {
-          return new Response('this is example', { headers: { 'content-type': 'application/javascript; charset=utf-8' } })
-        })
-      }
-    })
-    app = await injectApp()
+    const spy = sandbox.spy(redis.client, 'get')
 
     expect(spy.callCount).to.equal(0)
     let response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
-    // delete file from redis
-    await redis.client.del(`${response.headers.version}:/example.js:body`)
-    await redis.client.del(`${response.headers.version}:/example.js:meta`)
-    // and fetch once more
+    expect(spy.withArgs(sinon.match(/\/example.js/)).callCount).to.equal(1)
     response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
+    expect(spy.withArgs(sinon.match(/\/example.js/)).callCount).to.equal(1)
   })
 
   it('delivers binary files from cache', async function () {
-    let spy
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/image.png': spy = sandbox.spy(() => {
-          return new Response(image, {
-            headers: {
-              'Content-Type': 'image/png',
-              'Content-Length': imageStat.size.toString()
-            }
-          })
-        })
-      }
-    })
-    app = await injectApp()
+    const spy = sandbox.spy(redis.client, 'get')
+
     expect(spy.callCount).to.equal(0)
     let response = await app.inject({ url: '/image.png' })
     expect(response.statusCode).to.equal(200)
     expect(response.rawPayload).to.deep.equal(image)
-    expect(spy.callCount).to.equal(1)
+    expect(spy.withArgs(sinon.match(/\/image.png/)).callCount).to.equal(1)
     response = await app.inject({ url: '/image.png' })
     expect(response.statusCode).to.equal(200)
     expect(response.rawPayload).to.deep.equal(image)
-    expect(spy.callCount).to.equal(1)
-  })
-
-  it('a file is not cached again, if loaded from cache', async function () {
-    const spy = sandbox.spy(redis.client, 'set')
-
-    let response = await app.inject({ url: '/example.js' })
-    expect(response.statusCode).to.equal(200)
-
-    // called 4 times.
-    // once for manifests
-    // once for dependencies
-    // two times for for example.js (meta and body)
-    expect(spy.callCount).to.equal(4)
-
-    response = await app.inject({ url: '/example.js' })
-    expect(response.statusCode).to.equal(200)
-
-    // should still be called 4 times, because everything is in cache
-    expect(spy.callCount).to.equal(4)
-  })
-
-  it('requests known file only from one server', async function () {
-    let spy1, spy2
-    await mockConfig({ baseUrls: ['http://ui-server1/', 'http://ui-server2/'] })
-    // we have example.js in both files. the first one will be overwritten and therefore not be called
-    mockFetch({
-      'http://ui-server1': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': { }
-        }),
-        '/example.js': spy1 = sandbox.spy(() => {
-          return new Response('example', { headers: { 'content-type': 'text/plain' } })
-        })
-      },
-      'http://ui-server2': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': { }
-        }),
-        '/example.js': spy2 = sandbox.spy(() => {
-          return new Response('example', { headers: { 'content-type': 'text/plain' } })
-        })
-      }
-    })
-    app = await injectApp()
-
-    const response = await app.inject({ url: '/example.js' })
-    expect(response.statusCode).to.equal(200)
-    expect(spy1.callCount).to.equal(0)
-    expect(spy2.callCount).to.equal(1)
+    expect(spy.withArgs(sinon.match(/\/image.png/)).callCount).to.equal(1)
   })
 
   it('serves cached files with version', async function () {
-    // we have example.js in both files. the first one will be overwritten and therefore not be called
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': { }
-        }),
-        '/example.js': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn(
-          new Response('first', { headers: { 'content-type': 'text/plain' } }),
-          new Response('second', { headers: { 'content-type': 'text/plain' } })
-        )
-      }
-    })
-    app = await injectApp()
-
+    redis.client.set('ui-middleware:1234:/example.js:body', 'first')
+    redis.client.set('ui-middleware:1234:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } }))
     const response1 = await app.inject({
       url: '/example.js',
       headers: { version: '1234' }
@@ -275,85 +142,33 @@ describe('File caching service', function () {
 
     const response2 = await app.inject({ url: '/example.js' })
     expect(response2.statusCode).to.equal(200)
-    expect(response2.body).to.equal('second')
+    expect(response2.body).to.equal('this is example')
 
-    const latestVersion = response2.headers['latest-version']
+    const latestVersion = response1.headers['latest-version']
 
     const response3 = await app.inject({
-      url: '/example.js',
-      headers: { version: '1234' }
-    })
-    expect(response3.statusCode).to.equal(200)
-    expect(response3.body).to.equal('first')
-
-    const response4 = await app.inject({ url: '/example.js' })
-    expect(response4.statusCode).to.equal(200)
-    expect(response4.body).to.equal('second')
-
-    const response5 = await app.inject({
       url: '/example.js',
       headers: { version: latestVersion }
     })
-    expect(response5.statusCode).to.equal(200)
-    expect(response5.body).to.equal('second')
+    expect(response3.statusCode).to.equal(200)
+    expect(response3.body).to.equal('this is example')
   })
 
   it('checks again for files after an error occurred', async function () {
-    // we have example.js in both files. the first one will be overwritten and therefore not be called
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': { }
-        }),
-        '/example.js': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn(
-          new Response('UI-container not available', { headers: { 'content-type': 'text/plain' }, status: 503 }),
-          new Response('Now available', { headers: { 'content-type': 'text/plain' } })
-        )
-      }
-    })
-    app = await injectApp()
+    redis.client.set('ui-middleware:123:viteManifests', JSON.stringify({}))
+    const response1 = await app.inject({ url: '/example.js', headers: { version: '123' } })
+    expect(response1.statusCode).to.equal(404)
 
-    const response1 = await app.inject({ url: '/example.js' })
-    expect(response1.statusCode).to.equal(503)
+    redis.client.set('ui-middleware:123:/example.js:body', 'Now available')
+    redis.client.set('ui-middleware:123:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } }))
 
-    const response2 = await app.inject({ url: '/example.js' })
+    const response2 = await app.inject({ url: '/example.js', headers: { version: '123' } })
     expect(response2.statusCode).to.equal(200)
     expect(response2.body).to.equal('Now available')
   })
 
-  it('does not check again, when a 404 occurred', async function () {
-    // we have example.js in both files. the first one will be overwritten and therefore not be called
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': { }
-        }),
-        '/example.js': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn(
-          new Response('Not found', { headers: { 'content-type': 'text/plain' }, status: 404 }),
-          new Response('Now found', { headers: { 'content-type': 'text/plain' } })
-        )
-      }
-    })
-    app = await injectApp()
-
-    const response1 = await app.inject({ url: '/example.js' })
-    expect(response1.statusCode).to.equal(404)
-
-    const response2 = await app.inject({ url: '/example.js' })
-    expect(response2.statusCode).to.equal(404)
-  })
-
   it('only fetches files once, even when requested simultanously', async function () {
-    let spy
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/example.js': spy = sandbox.spy(() => {
-          return new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
-        })
-      }
-    })
-    app = await injectApp()
+    const spy = sandbox.spy(redis.client, 'get')
 
     expect(spy.callCount).to.equal(0)
     const [res1, res2] = await Promise.all([
@@ -362,21 +177,11 @@ describe('File caching service', function () {
     ])
     expect(res1.statusCode).to.equal(200)
     expect(res2.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
+    expect(spy.withArgs(sinon.match(/\/example.js/)).callCount).to.equal(1)
   })
 
   it('only fetches manifests once, even when requested simultaneously', async function () {
-    let spy
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': spy = sandbox.spy(async () => {
-          await wait(10)
-          return new Response(JSON.stringify(generateSimpleViteManifest({})), { headers: { 'content-type': 'application/json' } })
-        }),
-        '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
-      }
-    })
-    app = await injectApp()
+    const spy = sandbox.spy(redis.client, 'get')
 
     expect(spy.callCount).to.equal(0)
     const [res1, res2] = await Promise.all([
@@ -385,7 +190,7 @@ describe('File caching service', function () {
     ])
     expect(res1.statusCode).to.equal(200)
     expect(res2.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
+    expect(spy.withArgs(sinon.match(/oxManifests/)).callCount).to.equal(1)
   })
 
   describe('redis request latency', function () {
@@ -402,16 +207,7 @@ describe('File caching service', function () {
     })
 
     it('only requests files once, even though the response takes some time', async function () {
-      let spy
-      mockFetch({
-        'http://ui-server': {
-          '/manifest.json': generateSimpleViteManifest({}),
-          '/example.js': spy = sandbox.spy(() => {
-            return new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
-          })
-        }
-      })
-      app = await injectApp()
+      const spy = sandbox.spy(redis.client, 'get')
 
       expect(spy.callCount).to.equal(0)
       const [res1, res2] = await Promise.all([
@@ -425,14 +221,8 @@ describe('File caching service', function () {
   })
 
   it('serves index.html gzip compressed', async function () {
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/index.html': () => new Response([...new Array(2500)].join(' '), { headers: { 'content-type': 'text/html' } })
-      }
-    })
-    app = await injectApp()
-
+    redis.client.set('ui-middleware:554855300:/index.html:body', zlib.gzipSync('<html><head></head><body>it\'s me</body></html>'))
+    redis.client.set('ui-middleware:554855300:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html', 'content-encoding': 'gzip' } }))
     const response = await app.inject({ url: '/index.html' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-encoding']).to.equal('gzip')
@@ -441,14 +231,8 @@ describe('File caching service', function () {
   })
 
   it('serves files as brotli compressed', async function () {
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/large.js': () => new Response([...new Array(2500)].join('a'), { headers: { 'content-type': 'application/javascript' } })
-      }
-    })
-    app = await injectApp()
-
+    redis.client.set('ui-middleware:554855300:/large.js:body', zlib.brotliCompressSync('this is example'))
+    redis.client.set('ui-middleware:554855300:/large.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', 'content-encoding': 'br' } }))
     const response = await app.inject({ url: '/large.js' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-encoding']).to.equal('br')
@@ -458,14 +242,8 @@ describe('File caching service', function () {
   })
 
   it('does not serve small files with compression', async function () {
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/small.js': () => new Response('small', { headers: { 'content-type': 'application/javascript' } })
-      }
-    })
-    app = await injectApp()
-
+    redis.client.set('ui-middleware:554855300:/small.js:body', 'this is example')
+    redis.client.set('ui-middleware:554855300:/small.js:meta', JSON.stringify({ headers: { 'content-type': 'text/plain' } }))
     const response = await app.inject({ url: '/small.js' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers).to.not.have.property('content-encoding')
@@ -475,14 +253,8 @@ describe('File caching service', function () {
   })
 
   it('does not serve other mime types with compression', async function () {
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/file.mp3': () => new Response('123', { headers: { 'content-type': 'audio/mpeg' } })
-      }
-    })
-    app = await injectApp()
-
+    redis.client.set('ui-middleware:554855300:/file.mp3:body', 'this is example')
+    redis.client.set('ui-middleware:554855300:/file.mp3:meta', JSON.stringify({ headers: { 'content-type': 'audio/mpeg' } }))
     const response = await app.inject({ url: '/file.mp3' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers).to.not.have.property('content-encoding')
@@ -492,14 +264,8 @@ describe('File caching service', function () {
   })
 
   it('does serve svg with brotli compression (also escapes chars in regex)', async function () {
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/file.svg': () => new Response([...new Array(2500)].join(' '), { headers: { 'content-type': 'image/svg+xml' } })
-      }
-    })
-    app = await injectApp()
-
+    redis.client.set('ui-middleware:554855300:/file.svg:body', zlib.brotliCompressSync([...new Array(2500)].join(' ')))
+    redis.client.set('ui-middleware:554855300:/file.svg:meta', JSON.stringify({ headers: { 'content-type': 'image/svg+xml', 'content-encoding': 'br' } }))
     const response = await app.inject({ url: '/file.svg' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-encoding']).to.equal('br')
diff --git a/spec/headers_test.js b/spec/headers_test.js
index 81c2a435d328e52d8ef7178f42434defe2240eea..95d6e7668ca3a740ba8de1d14b59a460c27bcc4d 100644
--- a/spec/headers_test.js
+++ b/spec/headers_test.js
@@ -20,35 +20,23 @@
  *
  */
 
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { injectApp, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
 
 describe('Responses contain custom headers', function () {
-  let app
-
-  before(async function () {
-    await mockConfig({ baseUrls: ['http://ui-server/'] })
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': {},
-          'main.css': {},
-          'index.html': {
-            file: 'index.html.js',
-            isEntry: true,
-            imports: ['example.js'],
-            css: ['main.css']
-          }
-        }),
-        '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
-        '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }),
-        '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
-        '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } })
-      }
-    });
-    (await mockRedis()).isReady()
+  let app, client
+
+  beforeEach(async function () {
+    client = (await mockRedis()).client
+    await Promise.all([
+      client.set('ui-middleware:versionInfo', JSON.stringify({ version: 3215668592 })),
+      client.set('ui-middleware:3215668592:/index.html:body', '<html><head></head><body>it\'s me</body></html>'),
+      client.set('ui-middleware:3215668592:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } })),
+      client.set('ui-middleware:3215668592:/index.html.js:body', 'console.log("it\'s me")'),
+      client.set('ui-middleware:3215668592:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', dependencies: ['main.css'] } }))
+    ])
     app = await injectApp()
   })
 
@@ -68,95 +56,19 @@ describe('Responses contain custom headers', function () {
   })
 
   it('serves requested version', async function () {
+    client.set('ui-middleware:123456:/index.html.js:body', 'console.log("it\'s old me")')
+    client.set('ui-middleware:123456:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } }))
     const response = await app.inject({ url: '/index.html.js', headers: { version: '123456' } })
     expect(response.statusCode).to.equal(200)
     expect(response.headers.version).to.equal('123456')
+    expect(response.body).to.equal('console.log("it\'s old me")')
     expect(response.headers['latest-version']).to.equal('3215668592')
   })
 
   it('javascript file contains dependencies', async function () {
     const response = await app.inject({ url: '/index.html.js' })
     expect(response.statusCode).to.equal(200)
+    expect(response.body).to.equal('console.log("it\'s me")')
     expect(response.headers.dependencies[0]).to.equal('main.css')
   })
-
-  describe('with different files', function () {
-    beforeEach(async function () {
-      td.reset()
-      await new RedisMock().flushdb()
-
-      await mockConfig({ baseUrls: ['http://ui-server/'] })
-      await mockRedis()
-      mockFetch({
-        'http://ui-server': {
-          '/manifest.json': generateSimpleViteManifest({
-            'index.html': {}
-          }),
-          '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } })
-        }
-      })
-      app = await injectApp()
-    })
-
-    it('index.html has version', async function () {
-      const response = await app.inject({ url: '/index.html' })
-      expect(response.statusCode).to.equal(200)
-      // important here is, that it is different than in the test without meta.json
-      expect(response.headers.version).to.equal('3961519424')
-    })
-  })
-
-  describe('with meta.json', function () {
-    beforeEach(async function () {
-      td.reset()
-      await new RedisMock().flushdb()
-
-      await mockConfig({ baseUrls: ['http://ui-server/'] })
-      await mockRedis()
-      mockFetch({
-        'http://ui-server': {
-          '/manifest.json': generateSimpleViteManifest({
-            'index.html': {}
-          }),
-          '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
-          '/meta.json': { commitSha: '1234567890' }
-        }
-      })
-      app = await injectApp()
-    })
-
-    it('index.html has version', async function () {
-      const response = await app.inject({ url: '/index.html' })
-      expect(response.statusCode).to.equal(200)
-      // important here is, that it is different than in the test without meta.json
-      expect(response.headers.version).to.equal('1487554813')
-    })
-  })
-
-  describe('with different meta.json', function () {
-    beforeEach(async function () {
-      td.reset()
-      await new RedisMock().flushdb()
-
-      await mockConfig({ baseUrls: ['http://ui-server/'] })
-      await mockRedis()
-      mockFetch({
-        'http://ui-server': {
-          '/manifest.json': generateSimpleViteManifest({
-            'index.html': {}
-          }),
-          '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
-          '/meta.json': { commitSha: '0987654321' }
-        }
-      })
-      app = await injectApp()
-    })
-
-    it('index.html has version', async function () {
-      const response = await app.inject({ url: '/index.html' })
-      expect(response.statusCode).to.equal(200)
-      // important here is, that it is different than in the test without meta.json
-      expect(response.headers.version).to.equal('319344871')
-    })
-  })
 })
diff --git a/spec/meta_test.js b/spec/meta_test.js
index da108c2e2080adaeb6079d747789c6852b44f0c5..0ad1accc67c244e86a5b61ebbad37570bb4222d1 100644
--- a/spec/meta_test.js
+++ b/spec/meta_test.js
@@ -20,44 +20,39 @@
  *
  */
 
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { injectApp, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
 
 describe('Responses contain custom headers', function () {
-  let fetchConfig
-  let config
+  let client
   let app
 
   beforeEach(async function () {
-    await mockConfig(config = { baseUrls: ['http://ui-server/'] })
-    await mockRedis()
-    mockFetch(fetchConfig = {
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({
-          'example.js': {}
-        }),
-        '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
-        '/meta.json': { name: 'sample-service', version: '1.0' }
-      }
-    })
+    client = (await mockRedis()).client
+    await Promise.all([
+      client.set('ui-middleware:versionInfo', JSON.stringify({ version: '123456' })),
+      client.set('ui-middleware:123456:mergedMetadata', JSON.stringify([
+        { name: 'sample-service', version: '1.0' },
+        {
+          id: 'ui-middleware',
+          name: 'UI Middleware',
+          buildDate: '0123456789',
+          commitSha: '0123456789abcdef',
+          version: '4.2'
+        }
+      ]))
+    ])
     app = await injectApp()
   })
 
   afterEach(async function () {
     await new RedisMock().flushdb()
-    delete process.env.APP_VERSION
-    delete process.env.BUILD_TIMESTAMP
-    delete process.env.CI_COMMIT_SHA
     td.reset()
   })
 
   it('has own metadata', async function () {
-    process.env.APP_VERSION = '4.2'
-    process.env.BUILD_TIMESTAMP = '0123456789'
-    process.env.CI_COMMIT_SHA = '0123456789abcdef'
-
     const response = await app.inject({ url: '/meta' })
     expect(response.statusCode).to.equal(200)
     expect(response.json()).to.deep.contain({
@@ -82,57 +77,10 @@ describe('Responses contain custom headers', function () {
     const response = await app.inject({ url: '/meta' })
     expect(response.json()).to.have.length(2)
 
-    config.baseUrls = []
-    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
-      // need to process two times to actually trigger the update
-      await updateVersionProcessor()
-      await updateVersionProcessor()
-    })
-
-    const response2 = await app.inject({ url: '/meta' })
+    client.set('ui-middleware:123457:mergedMetadata', JSON.stringify([
+      { name: 'sample-service', version: '2.0' }
+    ]))
+    const response2 = await app.inject({ url: '/meta', headers: { version: '123457' } })
     expect(response2.json()).to.have.length(1)
   })
-
-  describe('without service avaible', function () {
-    beforeEach(function () {
-      delete fetchConfig['http://ui-server']
-    })
-
-    it('does not have metadata from ui service when unavailable', async function () {
-      await import('../src/cache.js').then(({ clear }) => clear())
-      const response = await app.inject({ url: '/meta' })
-      expect(response.statusCode).to.equal(200)
-      expect(response.json()).to.not.deep.contain({
-        name: 'sample-service',
-        version: '1.0'
-      })
-    })
-  })
-
-  describe('without redis disabled', function () {
-    beforeEach(async function () {
-      td.reset()
-      await mockConfig({ baseUrls: ['http://ui-server/'] })
-      await mockRedis()
-      mockFetch(fetchConfig = {
-        'http://ui-server': {
-          '/manifest.json': generateSimpleViteManifest({
-            'example.js': {}
-          }),
-          '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
-          '/meta.json': { name: 'sample-service', version: '1.0' }
-        }
-      })
-      app = await injectApp()
-    })
-
-    it('has metadata', async function () {
-      const response = await app.inject({ url: '/meta' })
-      expect(response.statusCode).to.equal(200)
-      expect(response.json()).to.deep.contain({
-        name: 'sample-service',
-        version: '1.0'
-      })
-    })
-  })
 })
diff --git a/spec/redis_test.js b/spec/redis_test.js
deleted file mode 100644
index 41b4ac7adcd438660285e5441bd24ce27c49b3c2..0000000000000000000000000000000000000000
--- a/spec/redis_test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- *
- * @copyright Copyright (c) OX Software GmbH, Germany <info@open-xchange.com>
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
- *
- * Any use of the work other than as authorized under this license or copyright law is prohibited.
- *
- */
-
-import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
-import { expect } from 'chai'
-import * as td from 'testdouble'
-import sinon from 'sinon'
-import RedisMock from 'ioredis-mock'
-
-const sandbox = sinon.createSandbox()
-
-describe('Redis', function () {
-  let app
-  let spy
-
-  beforeEach(async function () {
-    await mockConfig({ baseUrls: ['http://ui-server/'] })
-    await mockRedis({}, false)
-    mockFetch({
-      'http://ui-server': {
-        '/manifest.json': generateSimpleViteManifest({}),
-        '/example.js': spy = sandbox.spy(() => {
-          return new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
-        })
-      }
-    })
-    app = await injectApp()
-  })
-
-  afterEach(async function () {
-    td.reset()
-    await new RedisMock().flushdb()
-  })
-
-  it('use internal cache, when redis is disabled', async function () {
-    expect(spy.callCount).to.equal(0)
-    let response = await app.inject({ url: '/example.js' })
-    expect(response.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
-    response = await app.inject({ url: '/example.js' })
-    expect(response.statusCode).to.equal(200)
-    expect(spy.callCount).to.equal(1)
-  })
-})
diff --git a/spec/salt_test.js b/spec/salt_test.js
index 4cdeecb9e3a722e76dc24f27c3155968c31510b6..4a755b252ae5ee6cfba15cbee6da8088c0e262e7 100644
--- a/spec/salt_test.js
+++ b/spec/salt_test.js
@@ -47,6 +47,14 @@ describe('Salt', function () {
   })
 
   it('change version when salt changes', async function () {
+    const pubClient = new RedisMock()
+    let runUpdate
+    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
+      runUpdate = updateVersionProcessor
+    })
+    await runUpdate(pubClient)
+    await runUpdate(pubClient)
+
     const response = await app.inject({ url: '/manifests' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers.version).to.equal('1916675216')
@@ -54,11 +62,9 @@ describe('Salt', function () {
     // update salt
     config.salt = '1'
 
-    await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
-      // need to process two times to actually trigger the update
-      await updateVersionProcessor()
-      await updateVersionProcessor()
-    })
+    // need to process two times to actually trigger the update
+    await runUpdate(pubClient)
+    await runUpdate(pubClient)
 
     const responseAfterUpdate = await app.inject({ url: '/manifests' })
     expect(responseAfterUpdate.statusCode).to.equal(200)
diff --git a/spec/util.js b/spec/util.js
index c7ee1ac4c4ab0f39e035156a99c314156001222a..4a67a48bf48af2ebdaea808ad7a4089b90399dbf 100644
--- a/spec/util.js
+++ b/spec/util.js
@@ -79,9 +79,8 @@ export async function mockRedis (data = {}, isEnabled = true) {
   const mock = {
     isReady () { return Promise.resolve() },
     isEnabled () { return isEnabled },
-    client: new RedisMock(data),
-    pubClient: new RedisMock(),
-    subClient: new RedisMock()
+    createClient () { return new RedisMock() },
+    client: new RedisMock(data)
   }
   await td.replaceEsm('../src/redis.js', mock)
   return mock
diff --git a/spec/version_mismatches_test.js b/spec/version_mismatches_test.js
index cbb49cb5146648f5a691c1983bc03685f2372e4a..5438e5884d1b45903c67d7ec35041fc0df6734a3 100644
--- a/spec/version_mismatches_test.js
+++ b/spec/version_mismatches_test.js
@@ -24,13 +24,16 @@ import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
+import promClient from 'prom-client'
 
 describe('version mismatches', function () {
   let app
+  let pubClient
+  let runUpdate
 
   beforeEach(async function () {
     await mockConfig({ baseUrls: ['http://ui-server/'] })
-    await mockRedis()
+    const { createClient } = await mockRedis()
     mockFetch({
       'http://ui-server': {
         '/manifest.json': generateSimpleViteManifest({
@@ -49,6 +52,13 @@ describe('version mismatches', function () {
         )
       }
     })
+    pubClient = createClient('pubClient')
+    for (const prop of Object.getOwnPropertyNames(promClient.register._metrics)) {
+      delete promClient.register._metrics[prop]
+    }
+    const { updateVersionProcessor } = await import('../src/version.js')
+    runUpdate = updateVersionProcessor
+    runUpdate(pubClient)
     app = await injectApp()
   })
 
@@ -68,6 +78,9 @@ describe('version mismatches', function () {
     response = await app.inject({ url: '/bar.js' })
     expect(response.statusCode).to.equal(404)
 
+    await runUpdate(pubClient)
+    await runUpdate(pubClient)
+
     // get foo.js again. Since the versions should coincide now, the client should receive the new file
     response = await app.inject({ url: '/foo.js' })
     expect(response.statusCode).to.equal(200)
@@ -86,6 +99,9 @@ describe('version mismatches', function () {
     response = await app.inject({ url: '/whatever.js' })
     expect(response.statusCode).to.equal(404)
 
+    await runUpdate(pubClient)
+    await runUpdate(pubClient)
+
     // get foo.js again. Since the versions should coincide now, the client should receive the new file
     response = await app.inject({ url: '/foo.js' })
     expect(response.statusCode).to.equal(200)
diff --git a/src/cache.js b/src/cache.js
index 179d7bcf1bcc62a9201d1c46cb00e7ef7e4b9981..b996c405480505802da7ddf594c620f16102d9a1 100644
--- a/src/cache.js
+++ b/src/cache.js
@@ -20,6 +20,7 @@
  *
  */
 
+import { NotFoundError } from './errors.js'
 import logger from './logger.js'
 import * as redis from './redis.js'
 import { getRedisKey } from './util.js'
@@ -116,6 +117,10 @@ export function getFile ({ name, version }, fallback) {
       return (cache[key] = { body, ...JSON.parse(meta) })
     }
 
+    if (!fallback) {
+      delete cache[key]
+      throw new NotFoundError(`[Cache] Not found: ${key}`)
+    }
     const dataFromServer = await fallback({ version, name }).catch(err => {
       if (err.status !== 404) delete cache[key]
       throw err
@@ -141,5 +146,6 @@ export function getFile ({ name, version }, fallback) {
 
 function expire (key) {
   logger.debug(`[Cache] Key ${key} has expired.`)
+  fileCacheSizeGauge.dec()
   delete cache[key]
 }
diff --git a/src/errors.js b/src/errors.js
index e69343e5fba822cee08abeae863790193dfac0ba..aa6157acda8a00083cc088a58711e6ada5ab6576 100644
--- a/src/errors.js
+++ b/src/errors.js
@@ -21,7 +21,7 @@
  */
 
 export class NotFoundError extends Error {
-  constructor (message, options) {
+  constructor (message, options = {}) {
     super(message, options)
     this.status = options.status
   }
diff --git a/src/files.js b/src/files.js
index 161f68c7784ede7af6014a4d14382214b3b9873f..615728ebb4dfee2e58576bfad31f85bb7d791a34 100644
--- a/src/files.js
+++ b/src/files.js
@@ -28,7 +28,7 @@ import { configMap } from './config_map.js'
 import { NotFoundError, VersionMismatchError, isVersionMismatchError } from './errors.js'
 import logger from './logger.js'
 import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
-import { getVersionInfo, updateVersionProcessor } from './version.js'
+import { getVersionInfo } from './version.js'
 
 const gzip = promisify(zlib.gzip)
 const brotliCompress = promisify(zlib.brotliCompress)
@@ -95,35 +95,31 @@ export async function fetchFileWithHeaders ({ path, version }) {
   return Promise.any(configMap.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version })))
 }
 
-export function getFile ({ version, path }) {
+export function getFile ({ version, path, fetchFiles = true }) {
   return cache.getFile({ name: path, version }, ({ name: path, version }) => {
+    if (!fetchFiles) throw new Error('[File] Not found in cache: ' + path)
     return fetchFileWithHeaders({ version, path }).catch((err) => {
       if (!isVersionMismatchError(err)) throw err
       logger.warn(`[Files] The file ${path} has been delivered with the wrong version from the UI container.`)
-      updateVersionProcessor({ immediate: true })
 
       throw err
     })
   })
 }
 
-export async function warmCache ({ version }) {
+export async function warmCache ({ version, fetchFiles = false }) {
   const start = +new Date()
   logger.info('[File] start warming up the cache')
-  const viteManifests = await getViteManifests({ version })
-
-  for (const key of Object.keys(viteManifests)) {
-    const path = `/${viteManifests[key].file}`
-    if (!path) continue
-    try {
-      await getFile({ version, path })
-    } catch (err) {
-      if (isVersionMismatchError(err)) {
-        logger.info(`[File] Cache warming has been canceled because of a version mismatch at "${path}". Canceled after ${Math.floor((+new Date() - start) / 1000)}s`)
-        return
-      }
-      logger.info(`[File] could not prefetch file ${path}`)
-    }
+  const viteManifests = {}
+  try {
+    Object.assign(viteManifests, await getViteManifests({ version }))
+  } catch (err) {}
+  const paths = Object.keys(viteManifests).map(key => `/${viteManifests[key].file}`)
+
+  while (paths.length > 0) {
+    await Promise.all(paths.splice(0, 20).map(path => {
+      return getFile({ version, path, fetchFiles })
+    }))
   }
 
   logger.info(`[File] finished warming up the cache in ${Math.floor((+new Date() - start) / 1000)}s`)
diff --git a/src/index.js b/src/index.js
index 346be77948a301117deffae95d606b4206fa4a79..be93348effc5e86852dae55a21b75577aa57225e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -31,23 +31,32 @@ import logger from './logger.js'
 import fastify from 'fastify'
 import autoLoad from '@fastify/autoload'
 
-import { getLatestVersion } from './version.js'
+import { getLatestVersion, registerLatestVersionListener } from './version.js'
 import { configMap } from './config_map.js'
-import * as redis from './redis.js'
 import { warmCache } from './files.js'
+import { createClient, isReady, client } from './redis.js'
 import lightship from './lightship.js'
 
 const __filename = fileURLToPath(import.meta.url)
 const __dirname = dirname(__filename)
+const subClient = createClient('sub client')
 
 // Load env vars from .env and .env.defaults files
 // Note: actual env vars supersede .env file and .env file supersedes .env.defaults file
 config()
 
-lightship.queueBlockingTask(redis.isReady())
+async function waitForVersionAvailable () {
+  let version = await getLatestVersion()
+  while (!version) {
+    version = await getLatestVersion()
+    logger.info(`[Health] Check latest version on startup. Found ${version}`)
+    await new Promise(resolve => setTimeout(resolve, 1000))
+  }
+}
+
+lightship.queueBlockingTask(isReady())
 lightship.queueBlockingTask(configMap.load())
-lightship.queueBlockingTask(getLatestVersion()
-  .then(() => logger.info('[Health] Check latest version on startup.')))
+lightship.queueBlockingTask(waitForVersionAvailable())
 
 // Create a Fastify server
 const app = fastify({
@@ -71,11 +80,13 @@ const autoLoadOptions = { dir: join(__dirname, 'routes'), autoHooks: true }
 if (process.env.APP_ROOT !== '/') autoLoadOptions.options = { prefix: String(process.env.APP_ROOT).replace(/\/$/, '') }
 app.register(autoLoad, autoLoadOptions)
 
-app.addHook('onReady', () => {
-  // don't block the onReady hook
+lightship.whenFirstReady().then(async () => {
+  // don't block
   getLatestVersion()
     .then(version => warmCache({ version }))
     .catch(err => logger.error(err))
+
+  registerLatestVersionListener(subClient)
 })
 
 // This hook is used to signal lightship that the service is ready to receive requests
@@ -92,9 +103,8 @@ try {
 lightship.registerShutdownHandler(async () => {
   logger.info('[Service] Shutting down...')
   await Promise.all([
-    redis.client.quit(),
-    redis.pubClient.quit(),
-    redis.subClient.quit()
+    client.quit(),
+    subClient.quit()
   ])
   await app.close()
 })
diff --git a/src/lightship.js b/src/lightship.js
index d523db5d36164fb74b1a816891a1dee2701d7d43..eeed213d77d1a12469f7099d48a1d92de3608c59 100644
--- a/src/lightship.js
+++ b/src/lightship.js
@@ -23,7 +23,9 @@
 import { createLightship } from 'lightship'
 import logger from './logger.js'
 
-const lightship = await createLightship()
+const lightship = await createLightship({
+  port: Number(process.env.LIGHTSHIP_PORT)
+})
 
 // This is a graceful shutdown handler in case of uncaught exceptions
 process.on('uncaughtException', async err => {
diff --git a/src/redis.js b/src/redis.js
index 993cf543bfde38f00bb28f4e67e27f1103af423d..66f8b46ea4228759be1ee4a0887eb00bbef0181b 100644
--- a/src/redis.js
+++ b/src/redis.js
@@ -22,8 +22,6 @@
 
 import Redis from 'ioredis'
 import logger from './logger.js'
-import Queue from 'bull'
-import { registerLatestVersionListener, updateVersionProcessor } from './version.js'
 
 const commonQueueOptions = { enableReadyCheck: false, maxRetriesPerRequest: null }
 
@@ -32,7 +30,7 @@ const hosts = (process.env.REDIS_HOSTS || '').split(',').map(host => {
   return { host: hostname, port: Number(port) }
 })
 
-function createClient (id, options = {}) {
+export function createClient (id, options = commonQueueOptions) {
   options = {
     db: Number(process.env.REDIS_DB),
     password: process.env.REDIS_PASSWORD,
@@ -69,69 +67,6 @@ export async function isReady () {
 }
 
 export const client = createClient('common client', { maxRetriesPerRequest: 1 })
-export const pubClient = createClient('pub client')
-export const subClient = createClient('sub client', commonQueueOptions)
-
-/*
- * Bull specific things are below
- */
-
-client.on('ready', () => {
-  const updateVersionQueue = getQueue('update-version')
-  try {
-    updateVersionQueue.process(updateVersionProcessor)
-    logger.debug('[Redis] Register version update processor.')
-  } catch (err) {
-    logger.debug('[Redis] Update version processor is already registered.')
-  }
-  updateVersionQueue.add({}, {
-    jobId: 'update-version-job',
-    repeat: { every: Number(process.env.CACHE_TTL) },
-    removeOnComplete: 10,
-    removeOnFail: 10,
-    timeout: 10000
-  })
-})
-
-registerLatestVersionListener(subClient)
-
-/*
- * queue specific code
- */
-
-const queues = {}
-
-export function getQueue (name) {
-  if (queues[name]) return queues[name]
-  return (queues[name] = new Queue(name, {
-    prefix: process.env.REDIS_PREFIX,
-    createClient: function (type) {
-      switch (type) {
-        case 'client':
-          return client.duplicate()
-        case 'subscriber':
-          return subClient.duplicate()
-        default:
-          return client.duplicate(commonQueueOptions)
-      }
-    }
-  }))
-}
-
-export function getQueues () {
-  return Object.values(queues)
-}
-
-export async function closeQueue (name) {
-  if (!queues[name]) throw new Error(`No such queue "${name}" to close.`)
-  const queue = queues[name]
-  delete queues[name]
-  const jobs = await queue.getRepeatableJobs()
-  for (const job of jobs) {
-    queue.removeRepeatableByKey(job.key)
-  }
-  return queue.close()
-}
 
 export function isEnabled () {
   return !!process.env.REDIS_HOST
diff --git a/src/routes/metadata.js b/src/routes/metadata.js
index e4b6792237e6dc036b3e8dec0d31e3cd812546ec..fadceea0710b13725d2c0496b3b6c2c8a1d6633b 100644
--- a/src/routes/metadata.js
+++ b/src/routes/metadata.js
@@ -20,37 +20,12 @@
  *
  */
 
-import { configMap } from '../config_map.js'
 import * as cache from '../cache.js'
 import { getRedisKey } from '../util.js'
 
-async function fetchMergedMetadata () {
-  const metadata = await Promise.all(configMap.urls.map(async url => {
-    const { origin } = new URL(url)
-    try {
-      const response = await fetch(new URL('meta.json', origin), { cache: 'no-store' })
-      if (!response.ok) return
-      return response.json()
-    } catch (e) {
-      // unhandled
-    }
-  }))
-
-  metadata.push({
-    id: 'ui-middleware',
-    name: 'UI Middleware',
-    buildDate: process.env.BUILD_TIMESTAMP,
-    commitSha: process.env.CI_COMMIT_SHA,
-    version: process.env.APP_VERSION
-  })
-
-  // only return when contains data
-  return metadata.filter(Boolean)
-}
-
 export default async function metadataPlugin (fastify) {
   fastify.get('/meta', async (req, res) => {
-    const mergedMetadata = await cache.get(getRedisKey({ version: res.version, name: 'mergedMetadata' }), async () => [await fetchMergedMetadata()])
+    const mergedMetadata = await cache.get(getRedisKey({ version: res.version, name: 'mergedMetadata' }))
     res.send(mergedMetadata)
   })
 }
diff --git a/src/updater.js b/src/updater.js
new file mode 100644
index 0000000000000000000000000000000000000000..036101910b64f66cff94b036375618591eca25b4
--- /dev/null
+++ b/src/updater.js
@@ -0,0 +1,69 @@
+/*
+ *
+ * @copyright Copyright (c) OX Software GmbH, Germany <info@open-xchange.com>
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
+ *
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
+ *
+ */
+
+import { config } from 'dotenv-defaults'
+import { createClient, client, isReady } from './redis.js'
+import logger from './logger.js'
+import fastify from 'fastify'
+import autoLoad from '@fastify/autoload'
+import { fileURLToPath } from 'node:url'
+import { join, dirname } from 'node:path'
+import lightship from './lightship.js'
+import { updateVersionProcessor } from './version.js'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+const pubClient = createClient('pub client')
+
+config()
+
+async function runUpdate () {
+  try {
+    await updateVersionProcessor(pubClient)
+  } catch (err) {} finally {
+    setTimeout(() => runUpdate(), Number(process.env.CACHE_TTL))
+  }
+}
+runUpdate()
+
+lightship.queueBlockingTask(isReady())
+
+const app = fastify({})
+app.register(autoLoad, { dir: join(__dirname, 'plugins') })
+app.addHook('onReady', () => { lightship.signalReady() })
+
+try {
+  // Binds and listens for connections on the specified host and port
+  await app.listen({ host: '::', port: Number(process.env.PORT) })
+} catch (err) {
+  logger.error(err)
+  await lightship.shutdown()
+}
+
+lightship.registerShutdownHandler(async () => {
+  logger.info('[Service] Shutting down...')
+  await Promise.all([
+    client.quit(),
+    pubClient.quit()
+  ])
+  await app.close()
+})
diff --git a/src/version.js b/src/version.js
index b7e5b581bc9bd1feeb1fcd17e924b07136d300ef..9b1f1f5bb4a898872a4bef6ddd9f42b013740c44 100644
--- a/src/version.js
+++ b/src/version.js
@@ -21,30 +21,20 @@
  */
 
 import { configMap } from './config_map.js'
-import { asyncThrottle, getRedisKey, hash } from './util.js'
+import { getRedisKey, hash } from './util.js'
 import logger from './logger.js'
 import * as cache from './cache.js'
 import * as redis from './redis.js'
 import { Gauge } from 'prom-client'
-import { getViteManifests } from './manifests.js'
 import { warmCache } from './files.js'
+import { getViteManifests } from './manifests.js'
 
-const versionInfo = {
+export const versionInfo = {
   version: null,
   details: {}
 }
 
-const manifestFileEntriesGauge = new Gauge({
-  name: 'manifest_file_entries',
-  help: 'Number of entries in merged vite manifest (number of all known files)',
-  async collect () {
-    const version = versionInfo.version
-    this.set({ version }, Object.keys(await getViteManifests({ version })).length)
-  },
-  labelNames: ['version']
-})
-
-const versionUpdateGauge = new Gauge({
+export const versionUpdateGauge = new Gauge({
   name: 'version_update_event',
   help: 'Timestamp of a version update event',
   labelNames: ['version']
@@ -92,21 +82,11 @@ export async function getVersionInfo () {
       Object.assign(versionInfo, JSON.parse(redisVersionInfo))
       logger.info(`[Version] Got initial version from redis: '${versionInfo.version}'`)
       versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
-      return versionInfo
     } catch (err) {
       logger.error('[Version] Error in getVersionInfo', err)
     }
   }
 
-  await configMap.load()
-  const fetchedVersionInfo = await fetchVersionInfo()
-  logger.info(`[Version] Fetched initial version: '${fetchedVersionInfo.version}' - [${JSON.stringify(fetchedVersionInfo.details)}]`)
-
-  Object.assign(versionInfo, fetchedVersionInfo)
-  const stringifiedVersionInfo = JSON.stringify(versionInfo)
-  redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
-  await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo)
-
   return versionInfo
 }
 
@@ -144,7 +124,45 @@ export function registerLatestVersionListener (client) {
   })
 }
 
-export const updateVersionProcessor = asyncThrottle(async function updateVersionProcessor ({ immediate = false } = {}) {
+// only observe the version update event, no need to store the reference
+// eslint-disable-next-line no-new
+new Gauge({
+  name: 'manifest_file_entries',
+  help: 'Number of entries in merged vite manifest (number of all known files)',
+  async collect () {
+    const version = versionInfo.version
+    this.set({ version }, Object.keys(await getViteManifests({ version })).length)
+  },
+  labelNames: ['version']
+})
+
+export async function fetchMergedMetadata () {
+  const metadata = await Promise.all(configMap.urls.map(async url => {
+    const { origin } = new URL(url)
+    try {
+      const response = await fetch(new URL('meta.json', origin), { cache: 'no-store' })
+      if (!response.ok) return
+      return response.json()
+    } catch (e) {
+      // unhandled
+    }
+  }))
+
+  metadata.push({
+    id: 'ui-middleware',
+    name: 'UI Middleware',
+    buildDate: process.env.BUILD_TIMESTAMP,
+    commitSha: process.env.CI_COMMIT_SHA,
+    version: process.env.APP_VERSION
+  })
+
+  // only return when contains data
+  return metadata.filter(Boolean)
+}
+
+let prevProcessedVersion = null
+
+export async function updateVersionProcessor (pubClient) {
   try {
     logger.info('[Version] Check for new version')
     await configMap.load()
@@ -154,33 +172,31 @@ export const updateVersionProcessor = asyncThrottle(async function updateVersion
       fetchVersionInfo()
     ])
 
-    // don't wait for the data, can be done in background
-    getViteManifests({ version: fetchedVersionInfo.version }).then(manifests => {
-      manifestFileEntriesGauge.set({ version: fetchedVersionInfo.version }, Object.keys(manifests).length)
-    })
+    if (prevProcessedVersion && storedVersion === fetchedVersionInfo.version) {
+      // make sure to limit memory consumption and always check redis
+      cache.clear()
 
-    if (storedVersion === fetchedVersionInfo.version) {
       logger.info(`[Version] No new version has been found. No update needed. Current version: ${storedVersion}`)
       return storedVersion
     }
     logger.info(`[Version] Found new source version. Current version: '${storedVersion}', new version: '${fetchedVersionInfo.version}'`)
-    const prevProcessedVersion = await redis.client.get(getRedisKey({ name: 'prevProcessedVersion' }))
     // that means, that between the previous update processing and this one, there was no version change
-    if (prevProcessedVersion === fetchedVersionInfo.version || immediate) {
-      logger.info('[Version] publish update to other nodes.')
+    if (!storedVersion || prevProcessedVersion === fetchedVersionInfo.version) {
       // update local version info
       Object.assign(versionInfo, fetchedVersionInfo)
       const stringifiedVersionInfo = JSON.stringify(versionInfo)
-      redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
+      cache.clear()
+      await warmCache({ version: versionInfo.version, fetchFiles: true })
       await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo)
+      await cache.get(getRedisKey({ version: versionInfo.version, name: 'mergedMetadata' }), async () => [await fetchMergedMetadata()])
       versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
+      logger.info('[Version] publish update to other nodes.')
+      pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
     } else {
       logger.info(`[Version] do not execute update yet. Store version ${fetchedVersionInfo.version} as previous version.`)
-      await redis.client.set(getRedisKey({ name: 'prevProcessedVersion' }), fetchedVersionInfo.version)
+      prevProcessedVersion = fetchedVersionInfo.version
     }
-
-    return versionInfo.version
   } catch (err) {
     logger.error(`[Version] comparing version is not possible. Error: ${err.message}`)
   }
-})
+}