diff --git a/integration/caching_test.js b/integration/caching_test.js
index 0a15a5e9529eee60b713dd049c80eb4a684ec143..66f1fa62228f59f3a44d68a0a82debbb7f700d76 100644
--- a/integration/caching_test.js
+++ b/integration/caching_test.js
@@ -41,7 +41,7 @@ describe('File caching service', function () {
     expect(response.statusCode).to.equal(200)
     const version = response.headers.version
 
-    expect(await client.get(getRedisKey({ version, name: '/index.html:meta' }))).to.equal('{"headers":{"content-type":"text/html","dependencies":false},"sha256Sum":"iFSuC3aK6EN/ASUamuZ+j3xZXI9eBdIlxtVDFjn7y1I="}')
+    expect(await client.get(getRedisKey({ version, name: '/index.html:meta' }))).to.equal('{"sha256Sum":"iFSuC3aK6EN/ASUamuZ+j3xZXI9eBdIlxtVDFjn7y1I=","headers":{"content-type":"text/html","dependencies":false}}')
     expect(await client.get(getRedisKey({ version, name: '/index.html:body' }))).to.equal('<html><head></head><body>it\'s me</body></html>')
   })
 
diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js
index 9e48498b0055a73df954611fdf6453a43cd74f5b..db08ac9b764180ef2db97163e6002434b205c5ec 100644
--- a/spec/file_caching_test.js
+++ b/spec/file_caching_test.js
@@ -267,4 +267,49 @@ describe('File caching service', function () {
     expect(response5.statusCode).to.equal(200)
     expect(response5.text).to.equal('second')
   })
+
+  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 mockApp()
+
+    expect(spy.callCount).to.equal(0)
+    const [res1, res2] = await Promise.all([
+      request(app).get('/example.js'),
+      request(app).get('/example.js')
+    ])
+    expect(res1.statusCode).to.equal(200)
+    expect(res2.statusCode).to.equal(200)
+    expect(spy.callCount).to.equal(1)
+  })
+
+  it('only fetches manifests once, even when requested simultanously', async function () {
+    let spy
+    mockFetch({
+      'http://ui-server': {
+        '/manifest.json': spy = sandbox.spy(async () => {
+          await new Promise(resolve => setTimeout(resolve, 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 mockApp()
+
+    expect(spy.callCount).to.equal(0)
+    const [res1, res2] = await Promise.all([
+      request(app).get('/manifests'),
+      request(app).get('/example.js')
+    ])
+    expect(res1.statusCode).to.equal(200)
+    expect(res2.statusCode).to.equal(200)
+    expect(spy.callCount).to.equal(1)
+  })
 })
diff --git a/src/cache.js b/src/cache.js
index a781e291a7f6aa8449006c082cba1947efe75953..3076a23ab432dc93b9615c9f38660900a2b32934 100644
--- a/src/cache.js
+++ b/src/cache.js
@@ -2,6 +2,13 @@ import * as redis from './redis.js'
 
 const cache = {}
 
+export async function setAsync (key, asyncValue) {
+  cache[key] = asyncValue
+  const value = await asyncValue
+  await set(key, value)
+  return value
+}
+
 export function set (key, value) {
   if (cache[key] === value) return
   cache[key] = value
diff --git a/src/create-app.js b/src/create-app.js
index dcfeb49cac1b300463bd8e954512e36c44efabe1..10b21bff537aaa769154d83fec112b96101e490b 100644
--- a/src/create-app.js
+++ b/src/create-app.js
@@ -7,9 +7,7 @@ import yaml from 'js-yaml'
 import fs from 'fs'
 
 import versionMiddleware from './middlewares/version.js'
-import loadFromCacheMiddleware from './middlewares/load-from-cache.js'
-import loadFromUIServersMiddleware from './middlewares/load-from-server.js'
-import saveToCacheMiddleware from './middlewares/save-to-cache.js'
+import serveFilesMiddleware from './middlewares/serve-files.js'
 import finalHandlerMiddleware from './middlewares/final-handler.js'
 
 import health from './routes/health.js'
@@ -40,14 +38,12 @@ export function createApp () {
   app.timeout = 30000
 
   app.use(versionMiddleware)
-  app.use(loadFromCacheMiddleware)
 
   app.use(manifestsRouter)
   app.use(metadataRouter)
   app.use(redirectsRouter)
 
-  app.use(loadFromUIServersMiddleware)
-  app.use(saveToCacheMiddleware)
+  app.use(serveFilesMiddleware)
   app.use(finalHandlerMiddleware)
 
   return app
diff --git a/src/files.js b/src/files.js
index d193ffc995f4beb72c1bfc5f078c0771869dc424..dfe10b898c349d5886ba2ecf759bf25cad7be7aa 100644
--- a/src/files.js
+++ b/src/files.js
@@ -50,12 +50,15 @@ export async function fetchFileWithHeaders ({ path, version }) {
   return Promise.any(config.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl(path, baseUrl, version)))
 }
 
-export async function saveToCache ({ version, path, body, headers, ...rest }) {
-  if (typeof body !== 'string' && !(body instanceof Buffer)) body = JSON.stringify(body)
-  return Promise.all([
-    cache.set(getRedisKey({ version, name: `${path}:body` }), body),
-    cache.set(getRedisKey({ version, name: `${path}:meta` }), JSON.stringify({ headers, ...rest }))
+export async function saveAsyncToCache ({ version, path, promise }) {
+  await Promise.all([
+    cache.setAsync(getRedisKey({ version, name: `${path}:body` }), promise.then(({ body }) => {
+      if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body)
+      return body
+    })),
+    cache.setAsync(getRedisKey({ version, name: `${path}:meta` }), promise.then(({ body, ...rest }) => JSON.stringify(rest)))
   ])
+  return promise
 }
 
 export async function loadFromCache ({ version, path }) {
diff --git a/src/manifests.js b/src/manifests.js
index f70c176725467a3def662999783c598a39ea64ef..9cb1bdd1d4e4a4855f5b6a241b85e8c4f5b1a4dc 100644
--- a/src/manifests.js
+++ b/src/manifests.js
@@ -4,10 +4,7 @@ import { getRedisKey, viteManifestToDeps, viteToOxManifest } from './util.js'
 import { logger } from './logger.js'
 import * as cache from './cache.js'
 
-export async function getViteManifests ({ version }) {
-  const manifests = await cache.get(getRedisKey({ version, name: 'viteManifests' }))
-  if (manifests) return JSON.parse(manifests)
-
+export async function fetchViteManifests () {
   // vite manifests contains a set of objects with the vite-manifests
   // from the corresponding registered services
   const viteManifests = await Promise.all(config.urls.map(async baseUrl => {
@@ -28,29 +25,37 @@ export async function getViteManifests ({ version }) {
   }))
 
   // combine all manifests by keys. With duplicates, last wins
-  const newManifests = viteManifests.reduce((memo, manifest) => Object.assign(memo, manifest), {})
-  await cache.set(getRedisKey({ version, name: 'viteManifests' }), JSON.stringify(newManifests))
-  return newManifests
+  return viteManifests.reduce((memo, manifest) => Object.assign(memo, manifest), {})
 }
 
-export async function getOxManifests ({ version }) {
-  const manifests = await cache.get(getRedisKey({ version, name: 'oxManifests' }))
-  if (manifests) return JSON.parse(manifests)
+export async function getViteManifests ({ version }) {
+  let manifests = await cache.get(getRedisKey({ version, name: 'viteManifests' }))
+  if (!manifests) {
+    manifests = await cache.setAsync(getRedisKey({ version, name: 'viteManifests' }), fetchViteManifests().then(m => JSON.stringify(m)))
+  }
+  return JSON.parse(manifests)
+}
 
-  const viteManifests = await getViteManifests({ version })
-  const newManifests = viteToOxManifest(viteManifests)
-  await cache.set(getRedisKey({ version, name: 'oxManifests' }), JSON.stringify(newManifests))
-  return newManifests
+export async function getOxManifests ({ version }) {
+  let manifests = await cache.get(getRedisKey({ version, name: 'oxManifests' }))
+  if (!manifests) {
+    manifests = await cache.setAsync(getRedisKey({ version, name: 'oxManifests' }), (async () => {
+      const viteManifests = await getViteManifests({ version })
+      return JSON.stringify(viteToOxManifest(viteManifests))
+    })())
+  }
+  return JSON.parse(manifests)
 }
 
 export async function getDependencies ({ version }) {
-  const deps = await cache.get(getRedisKey({ version, name: 'dependencies' }))
-  if (deps) return JSON.parse(deps)
-
-  const viteManifests = await getViteManifests({ version })
-  const newDeps = viteManifestToDeps(viteManifests)
-  await cache.set(getRedisKey({ version, name: 'dependencies' }), JSON.stringify(newDeps))
-  return newDeps
+  let deps = await cache.get(getRedisKey({ version, name: 'dependencies' }))
+  if (!deps) {
+    deps = await await cache.setAsync(getRedisKey({ version, name: 'dependencies' }), (async () => {
+      const viteManifests = await getViteManifests({ version })
+      return JSON.stringify(viteManifestToDeps(viteManifests))
+    })())
+  }
+  return JSON.parse(deps)
 }
 
 export async function getCSSDependenciesFor ({ file, version }) {
diff --git a/src/meta.js b/src/meta.js
index 06a56c78599dab7ebfddb317114df97ae92cdb13..0b99feb499c7b3f6e904242e1d7ba5d388f162d0 100644
--- a/src/meta.js
+++ b/src/meta.js
@@ -3,11 +3,8 @@ import fetch from 'node-fetch'
 import * as cache from './cache.js'
 import { getRedisKey } from './util.js'
 
-export async function getMergedMetadata ({ version }) {
-  const metadata = await cache.get(getRedisKey({ version, name: 'mergedMetadata' }))
-  if (metadata) return JSON.parse(metadata)
-
-  const newMetadata = await Promise.all(config.urls.map(async url => {
+export async function fetchMergedMetadata () {
+  const metadata = await Promise.all(config.urls.map(async url => {
     const { origin } = new URL(url)
     try {
       const response = await fetch(new URL('meta.json', origin))
@@ -18,7 +15,7 @@ export async function getMergedMetadata ({ version }) {
     }
   }))
 
-  newMetadata.push({
+  metadata.push({
     id: 'ui-middleware',
     name: 'UI Middleware',
     buildDate: process.env.BUILD_TIMESTAMP,
@@ -27,7 +24,13 @@ export async function getMergedMetadata ({ version }) {
   })
 
   // only return when contains data
-  const filtered = newMetadata.filter(Boolean)
-  await cache.set(getRedisKey({ version, name: 'mergedMetadata' }), JSON.stringify(filtered))
-  return filtered
+  return metadata.filter(Boolean)
+}
+
+export async function getMergedMetadata ({ version }) {
+  let metadata = await cache.get(getRedisKey({ version, name: 'mergedMetadata' }))
+  if (!metadata) {
+    metadata = await cache.setAsync(getRedisKey({ version, name: 'mergedMetadata' }), fetchMergedMetadata().then(m => JSON.stringify(m)))
+  }
+  return JSON.parse(metadata)
 }
diff --git a/src/middlewares/load-from-cache.js b/src/middlewares/load-from-cache.js
deleted file mode 100644
index 3555df14ff08875e17d1f1c8b27c749b88adc4f5..0000000000000000000000000000000000000000
--- a/src/middlewares/load-from-cache.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { loadFromCache } from '../files.js'
-
-export default async function (req, res, next) {
-  try {
-    const data = await loadFromCache({
-      path: req.path,
-      version: res.version
-    })
-    if (!data) return
-    const { body, headers, sha256Sum } = data
-    res.body = body
-    res.headers = headers
-    res.locals.sha256Sum = sha256Sum
-    res.cache = false
-  } catch (err) {
-    next(err)
-  } finally {
-    next()
-  }
-}
diff --git a/src/middlewares/load-from-server.js b/src/middlewares/load-from-server.js
deleted file mode 100644
index e02e8d0d12bb42a950510027eb21c264780109f6..0000000000000000000000000000000000000000
--- a/src/middlewares/load-from-server.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { NotFoundError } from '../errors.js'
-import { fetchFileWithHeaders } from '../files.js'
-import createError from 'http-errors'
-
-export default async function (req, res, next) {
-  try {
-    if (res.body) return
-    const path = req.path === '/' ? '/index.html' : req.path
-    const { body, headers, sha256Sum } = await fetchFileWithHeaders({ path, version: res.version })
-    res.body = body
-    res.headers = headers
-    res.locals.sha256Sum = sha256Sum
-  } catch (err) {
-    // response might be an aggregate error. therefore need to check all errors
-    const errors = err.errors || [err]
-    const fileNotFound = errors.reduce((memo, error) => memo && error instanceof NotFoundError, true)
-    if (fileNotFound) next(createError(404, 'File does not exist.'))
-    else next(err)
-  } finally {
-    next()
-  }
-}
diff --git a/src/middlewares/save-to-cache.js b/src/middlewares/save-to-cache.js
deleted file mode 100644
index 0a384945e922f6a652ce561ab11cd4c9f3cdd62f..0000000000000000000000000000000000000000
--- a/src/middlewares/save-to-cache.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { saveToCache } from '../files.js'
-import { logger } from '../logger.js'
-
-export default async function (req, res, next) {
-  try {
-    if (!res.body) return
-    if (res.cache === false) return
-    saveToCache({
-      version: res.version,
-      path: req.path,
-      body: res.body,
-      headers: res.headers,
-      sha256Sum: res.locals.sha256Sum
-    }).catch(() => logger.error)
-  } catch (err) {
-    next(err)
-  } finally {
-    next()
-  }
-}
diff --git a/src/middlewares/serve-files.js b/src/middlewares/serve-files.js
new file mode 100644
index 0000000000000000000000000000000000000000..569718e6861cbd784f7c2f28f5f477eea920d444
--- /dev/null
+++ b/src/middlewares/serve-files.js
@@ -0,0 +1,28 @@
+import { fetchFileWithHeaders, loadFromCache, saveAsyncToCache } from '../files.js'
+import { NotFoundError } from '../errors.js'
+import createError from 'http-errors'
+
+export default async function (req, res, next) {
+  try {
+    if (req.method !== 'GET') return next()
+    const version = res.version
+    let data = await loadFromCache({
+      path: req.path,
+      version
+    })
+    if (!data) {
+      const path = req.path === '/' ? '/index.html' : req.path
+      data = await saveAsyncToCache({ version, path, promise: fetchFileWithHeaders({ path, version }) })
+    }
+
+    const { body, headers, sha256Sum } = data
+    res.set(headers)
+    res.locals.sha256Sum = sha256Sum
+    res.send(body)
+  } catch (err) {
+    const errors = err.errors || [err]
+    const fileNotFound = errors.reduce((memo, error) => memo && error instanceof NotFoundError, true)
+    if (fileNotFound) next(createError(404, 'File does not exist.'))
+    else next(err)
+  }
+}
diff --git a/src/routes/manifests.js b/src/routes/manifests.js
index 608a96354df2b5bd2574511ba569c4b26318d8bf..5d75dd2bbf4ac6a019d766c8dcb6f8ad72be8609 100644
--- a/src/routes/manifests.js
+++ b/src/routes/manifests.js
@@ -6,12 +6,9 @@ const router = new Router()
 router.get('/manifests', async function (req, res, next) {
   try {
     if (res.body) return
-    res.body = await getOxManifests({ version: res.version })
-    res.headers = { 'content-type': 'application/json' }
+    res.send(await getOxManifests({ version: res.version }))
   } catch (err) {
     next(err)
-  } finally {
-    next()
   }
 })
 
diff --git a/src/routes/metadata.js b/src/routes/metadata.js
index e93fcba426bd56b42b168a22a4771aeb057a5fe0..ebe28e7628bc47308d93ed9f05c87774db3bf9b3 100644
--- a/src/routes/metadata.js
+++ b/src/routes/metadata.js
@@ -5,12 +5,9 @@ const router = new Router()
 
 router.get('/meta', async (req, res, next) => {
   try {
-    res.body = await getMergedMetadata({ version: res.version })
-    res.headers = { 'content-type': 'application/json' }
+    res.send(await getMergedMetadata({ version: res.version }))
   } catch (err) {
     next(err)
-  } finally {
-    next()
   }
 })