diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
index 68bc040de6f1bb48898eb98dde59c06e2753304d..4039651427c294977b67c1a5ba36d224bdc90e52 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
@@ -7,19 +7,16 @@ import java.util.ArrayList;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 import java.util.logging.Logger;
-import java.util.stream.Collectors;
 
 import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.calc.Hazard;
-import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
 import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.calc.Vs30;
 import gov.usgs.earthquake.nshmp.data.MutableXySequence;
 import gov.usgs.earthquake.nshmp.data.XySequence;
-import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
 import gov.usgs.earthquake.nshmp.eq.model.SourceType;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
@@ -29,13 +26,11 @@ import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
 import gov.usgs.earthquake.nshmp.www.BaseModel;
 import gov.usgs.earthquake.nshmp.www.HazardController;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
-import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer;
-import gov.usgs.earthquake.nshmp.www.WsUtil;
-import gov.usgs.earthquake.nshmp.www.WsUtil.Key;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer;
 import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
 
 import io.micronaut.http.HttpResponse;
@@ -70,10 +65,10 @@ public final class HazardService {
       var usage = new SourceServices.ResponseData();
       var response = new Response<>(Status.USAGE, NAME, urlHelper.url, usage, urlHelper);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse));
+      LOGGER.info(NAME + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return WsUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
     }
   }
 
@@ -97,10 +92,10 @@ public final class HazardService {
       var data = new RequestData(query, Vs30.fromValue(query.vs30));
       var response = process(data, timer, urlHelper);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse));
+      LOGGER.info(NAME + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return WsUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
     }
   }
 
@@ -108,7 +103,10 @@ public final class HazardService {
       RequestData data,
       Timer timer,
       UrlHelper urlHelper) throws InterruptedException, ExecutionException {
-    var hazard = calc(data);
+    var configFunction = new ConfigFunction();
+    var siteFunction = new SiteFunction(data);
+    var hazard = ServicesUtil.calcHazard(configFunction, siteFunction);
+
     return new ResultBuilder()
         .hazard(hazard)
         .requestData(data)
@@ -117,41 +115,31 @@ public final class HazardService {
         .build();
   }
 
-  static Hazard calc(RequestData data) throws InterruptedException, ExecutionException {
-    var futuresList = ServletUtil.installedModel().models().stream()
-        .map(baseModel -> calcHazard(baseModel, ServletUtil.hazardModels().get(baseModel), data))
-        .collect(Collectors.toList());
-
-    var hazardsFuture = CompletableFuture
-        .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()]))
-        .thenApply(v -> {
-          return futuresList.stream()
-              .map(future -> future.join())
-              .collect(Collectors.toList());
-        });
-
-    var hazards = hazardsFuture.get().toArray(new Hazard[] {});
-    return Hazard.merge(hazards);
+  static class ConfigFunction implements Function<BaseModel, CalcConfig> {
+    @Override
+    public CalcConfig apply(BaseModel baseModel) {
+      var hazardModel = ServletUtil.hazardModels().get(baseModel);
+      var configBuilder = CalcConfig.copyOf(hazardModel.config());
+      configBuilder.imts(baseModel.imts);
+      return configBuilder.build();
+    }
   }
 
-  static CompletableFuture<Hazard> calcHazard(
-      BaseModel baseModel,
-      HazardModel hazardModel,
-      RequestData data) {
-    var location = Location.create(data.latitude, data.longitude);
-    var configBuilder = CalcConfig.copyOf(hazardModel.config());
-    configBuilder.imts(baseModel.imts);
-    var config = configBuilder.build();
-    var site = Site.builder()
-        .basinDataProvider(config.siteData.basinDataProvider)
-        .location(location)
-        .vs30(data.vs30.value())
-        .build();
+  static class SiteFunction implements Function<CalcConfig, Site> {
+    final RequestData data;
 
-    return CompletableFuture
-        .supplyAsync(
-            () -> HazardCalcs.hazard(hazardModel, config, site, ServletUtil.CALC_EXECUTOR),
-            ServletUtil.TASK_EXECUTOR);
+    private SiteFunction(RequestData data) {
+      this.data = data;
+    }
+
+    @Override
+    public Site apply(CalcConfig config) {
+      return Site.builder()
+          .basinDataProvider(config.siteData.basinDataProvider)
+          .location(Location.create(data.latitude, data.longitude))
+          .vs30(data.vs30.value())
+          .build();
+    }
   }
 
   public static class Query extends ServiceQueryData {
@@ -174,7 +162,7 @@ public final class HazardService {
     }
   }
 
-  static final class RequestData extends ServiceRequestData {
+  static class RequestData extends ServiceRequestData {
     final Vs30 vs30;
 
     RequestData(Query query, Vs30 vs30) {
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
index b24499194e513af09f819943eddda6ffa80c6d29..d88510f5baf37aa7d3ddd9c55f065365e680ee6c 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
@@ -21,15 +21,13 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
 import gov.usgs.earthquake.nshmp.mfd.Mfds;
 import gov.usgs.earthquake.nshmp.www.RateController;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
-import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer;
-import gov.usgs.earthquake.nshmp.www.WsUtil;
-import gov.usgs.earthquake.nshmp.www.WsUtil.Key;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData;
 import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata.DefaultParameters;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer;
 
 import io.micronaut.http.HttpResponse;
 
@@ -66,7 +64,7 @@ public final class RateService {
       var svcResponse = ServletUtil.GSON.toJson(response);
       return HttpResponse.ok(String.format(svcResponse, urlHelper.urlPrefix, urlHelper.urlPrefix));
     } catch (Exception e) {
-      return WsUtil.handleError(e, service.name, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper);
     }
   }
 
@@ -98,7 +96,7 @@ public final class RateService {
       LOGGER.info(service.name + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return WsUtil.handleError(e, service.name, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper);
     }
   }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3ef2cb6e46c2893e78789beecd021866731255d
--- /dev/null
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
@@ -0,0 +1,139 @@
+package gov.usgs.earthquake.nshmp.www.services;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import com.google.gson.GsonBuilder;
+
+import gov.usgs.earthquake.nshmp.calc.CalcConfig;
+import gov.usgs.earthquake.nshmp.calc.Hazard;
+import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
+import gov.usgs.earthquake.nshmp.calc.Site;
+import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
+import gov.usgs.earthquake.nshmp.internal.www.Response;
+import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
+import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
+import gov.usgs.earthquake.nshmp.www.BaseModel;
+import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
+
+import io.micronaut.http.HttpResponse;
+
+class ServicesUtil {
+
+  static HttpResponse<String> handleError(
+      Throwable e,
+      String name,
+      Logger logger,
+      UrlHelper urlHelper) {
+    var msg = e.getMessage() + " (see logs)";
+    var svcResponse = new Response<>(Status.ERROR, name, urlHelper.url, msg, urlHelper);
+    var gson = new GsonBuilder().setPrettyPrinting().create();
+    var response = gson.toJson(svcResponse);
+    logger.severe(name + " -\n" + response);
+    e.printStackTrace();
+    return HttpResponse.serverError(response);
+  }
+
+  static Hazard calcHazard(
+      Function<BaseModel, CalcConfig> configFunction,
+      Function<CalcConfig, Site> siteFunction) throws InterruptedException, ExecutionException {
+    var futuresList = ServletUtil.installedModel().models().stream()
+        .map(baseModel -> {
+          var config = configFunction.apply(baseModel);
+          var site = siteFunction.apply(config);
+          return calcHazard(baseModel, config, site);
+        })
+        .collect(Collectors.toList());
+
+    var hazardsFuture = CompletableFuture
+        .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()]))
+        .thenApply(v -> {
+          return futuresList.stream()
+              .map(future -> future.join())
+              .collect(Collectors.toList());
+        });
+
+    var hazards = hazardsFuture.get().toArray(new Hazard[] {});
+    return Hazard.merge(hazards);
+  }
+
+  static class ServiceQueryData implements ServiceQuery {
+    public final Double longitude;
+    public final Double latitude;
+
+    ServiceQueryData(Double longitude, Double latitude) {
+      this.longitude = longitude;
+      this.latitude = latitude;
+    }
+
+    @Override
+    public boolean isNull() {
+      return longitude == null && latitude == null;
+    }
+
+    @Override
+    public void checkValues() {
+      WsUtils.checkValue(Key.LONGITUDE, longitude);
+      WsUtils.checkValue(Key.LATITUDE, latitude);
+    }
+  }
+
+  static class ServiceRequestData {
+    public final SourceModel model;
+    public final double longitude;
+    public final double latitude;
+
+    public ServiceRequestData(ServiceQueryData query) {
+      model = new SourceModel(ServletUtil.installedModel());
+      longitude = query.longitude;
+      latitude = query.latitude;
+    }
+  }
+
+  enum Key {
+    EDITION,
+    REGION,
+    MODEL,
+    VS30,
+    LATITUDE,
+    LONGITUDE,
+    IMT,
+    RETURNPERIOD,
+    DISTANCE,
+    FORMAT,
+    TIMESPAN,
+    BASIN;
+
+    private String label;
+
+    private Key() {
+      label = name().toLowerCase();
+    }
+
+    @Override
+    public String toString() {
+      return label;
+    }
+  }
+
+  private static interface ServiceQuery {
+    boolean isNull();
+
+    void checkValues();
+  }
+
+  private static CompletableFuture<Hazard> calcHazard(
+      BaseModel baseModel,
+      CalcConfig config,
+      Site site) {
+    return CompletableFuture
+        .supplyAsync(
+            () -> HazardCalcs.hazard(
+                ServletUtil.hazardModels().get(baseModel), config, site, ServletUtil.CALC_EXECUTOR),
+            ServletUtil.TASK_EXECUTOR);
+  }
+
+}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..e11d2e22e19cf7f4c0e74453c0d8be5c1d287fdf
--- /dev/null
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
@@ -0,0 +1,183 @@
+package gov.usgs.earthquake.nshmp.www.services;
+
+import static java.lang.Runtime.getRuntime;
+
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.format.DateTimeFormatter;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import gov.usgs.earthquake.nshmp.calc.Site;
+import gov.usgs.earthquake.nshmp.calc.ValueFormat;
+import gov.usgs.earthquake.nshmp.calc.Vs30;
+import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
+import gov.usgs.earthquake.nshmp.www.BaseModel;
+import gov.usgs.earthquake.nshmp.www.Model;
+import gov.usgs.earthquake.nshmp.www.meta.MetaUtil;
+import gov.usgs.earthquake.nshmp.www.meta.Region;
+
+import io.micronaut.context.annotation.Value;
+import io.micronaut.context.event.ShutdownEvent;
+import io.micronaut.context.event.StartupEvent;
+import io.micronaut.runtime.event.annotation.EventListener;
+
+/**
+ * Micronaut controller utility objects and methods.
+ *
+ * @author U.S. Geological Survey
+ */
+public class ServletUtil {
+
+  public static final Gson GSON;
+  public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern(
+      "yyyy-MM-dd'T'HH:mm:ssXXX");
+
+  static final ListeningExecutorService CALC_EXECUTOR;
+  static final ExecutorService TASK_EXECUTOR;
+
+  static final int THREAD_COUNT;
+
+  /* Stateful flag to reject requests while a result is pending. */
+  static boolean uhtBusy = false;
+  static long hitCount = 0;
+  static long missCount = 0;
+
+  @Value("${nshmp-haz.installed-model}")
+  private Model model;
+
+  private static Model INSTALLED_MODEL;
+  private static Map<BaseModel, HazardModel> HAZARD_MODELS = new EnumMap<>(BaseModel.class);
+
+  static {
+    /* TODO modified for deagg-epsilon branch; should be context var */
+    THREAD_COUNT = getRuntime().availableProcessors();
+    CALC_EXECUTOR = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(THREAD_COUNT));
+    TASK_EXECUTOR = Executors.newSingleThreadExecutor();
+    GSON = new GsonBuilder()
+        .registerTypeAdapter(Region.class, new MetaUtil.EnumSerializer<Region>())
+        .registerTypeAdapter(Imt.class, new MetaUtil.EnumSerializer<Imt>())
+        .registerTypeAdapter(Vs30.class, new MetaUtil.EnumSerializer<Vs30>())
+        .registerTypeAdapter(ValueFormat.class, new MetaUtil.EnumSerializer<ValueFormat>())
+        .registerTypeAdapter(Double.class, new MetaUtil.DoubleSerializer())
+        .registerTypeAdapter(ParamType.class, new MetaUtil.ParamTypeSerializer())
+        .registerTypeAdapter(Site.class, new MetaUtil.SiteSerializer())
+        .disableHtmlEscaping()
+        .serializeNulls()
+        .setPrettyPrinting()
+        .create();
+  }
+
+  static Model installedModel() {
+    return INSTALLED_MODEL;
+  }
+
+  static Map<BaseModel, HazardModel> hazardModels() {
+    return HAZARD_MODELS;
+  }
+
+  @EventListener
+  void shutdown(ShutdownEvent event) {
+    CALC_EXECUTOR.shutdown();
+    TASK_EXECUTOR.shutdown();
+  }
+
+  @EventListener
+  void startup(StartupEvent event) {
+    INSTALLED_MODEL = model;
+
+    model.models().forEach(baseModel -> {
+      HAZARD_MODELS.put(baseModel, loadModel(baseModel));
+    });
+
+    HAZARD_MODELS = Map.copyOf(HAZARD_MODELS);
+  }
+
+  private HazardModel loadModel(BaseModel model) {
+    Path path;
+    URL url;
+    URI uri;
+    String uriString;
+    String[] uriParts;
+    FileSystem fs;
+
+    try {
+      url = Paths.get(model.path).toUri().toURL();
+      uri = new URI(url.toString().replace(" ", "%20"));
+      uriString = uri.toString();
+
+      /*
+       * When the web sevice is deployed inside a JAR file (and not unpacked by
+       * the servlet container) model resources will not exist on disk as
+       * otherwise expected. In this case, load the resources directly out of
+       * the JAR file as well. This is slower, but with the preload option
+       * enabled it may be less of an issue if the models are already in memory.
+       */
+
+      if (uriString.indexOf("!") != -1) {
+        uriParts = uri.toString().split("!");
+
+        try {
+          fs = FileSystems.getFileSystem(
+              URI.create(uriParts[0]));
+        } catch (FileSystemNotFoundException fnx) {
+          fs = FileSystems.newFileSystem(
+              URI.create(uriParts[0]),
+              new HashMap<String, String>());
+        }
+
+        path = fs.getPath(uriParts[1].replaceAll("%20", " "));
+      } else {
+        path = Paths.get(uri);
+      }
+
+      return HazardModel.load(path);
+
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static Timer timer() {
+    return new Timer();
+  }
+
+  /*
+   * Simple timer object. The servlet timer just runs. The calculation timer can
+   * be started later.
+   */
+  public static final class Timer {
+    Stopwatch servlet = Stopwatch.createStarted();
+    Stopwatch calc = Stopwatch.createUnstarted();
+
+    public Timer start() {
+      calc.start();
+      return this;
+    }
+
+    public String servletTime() {
+      return servlet.toString();
+    }
+
+    public String calcTime() {
+      return calc.toString();
+    }
+  }
+
+}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
index 8201509398c34895250165517a3c19b26f14aa3f..99c80fb53d9eb1befd88f6ed6f7bd379ededb303 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
@@ -23,8 +23,6 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
 import gov.usgs.earthquake.nshmp.www.BaseModel;
 import gov.usgs.earthquake.nshmp.www.Model;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
-import gov.usgs.earthquake.nshmp.www.WsUtil;
 import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.meta.MetaUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
@@ -71,7 +69,7 @@ public class SourceServices {
       LOGGER.info(NAME + "- Response:\n" + jsonString);
       return HttpResponse.ok(jsonString);
     } catch (Exception e) {
-      return WsUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
     }
   }