diff --git a/src/main/java/ru/r2cloud/cloud/LeoSatDataClient.java b/src/main/java/ru/r2cloud/cloud/LeoSatDataClient.java
index cd08a05f..8e8d29c7 100644
--- a/src/main/java/ru/r2cloud/cloud/LeoSatDataClient.java
+++ b/src/main/java/ru/r2cloud/cloud/LeoSatDataClient.java
@@ -332,6 +332,7 @@ private Tle readTle(JsonValue tle) {
 		Tle result = new Tle(new String[] { line1, line2, line3 });
 		// assume downloaded TLE is always fresh
 		result.setLastUpdateTime(clock.millis());
+		result.setSource(hostname);
 		return result;
 	}
 
diff --git a/src/main/java/ru/r2cloud/cloud/SatnogsClient.java b/src/main/java/ru/r2cloud/cloud/SatnogsClient.java
index 60a52f1a..3629e800 100644
--- a/src/main/java/ru/r2cloud/cloud/SatnogsClient.java
+++ b/src/main/java/ru/r2cloud/cloud/SatnogsClient.java
@@ -160,6 +160,7 @@ private Tle readTle(String body) {
 		// if satellite is new launch, then there is no other source for tle
 		// readTle is called only for new launches
 		result.setLastUpdateTime(clock.millis());
+		result.setSource(hostname);
 		return result;
 	}
 
diff --git a/src/main/java/ru/r2cloud/model/Tle.java b/src/main/java/ru/r2cloud/model/Tle.java
index 1bcff5fe..4e7b3184 100644
--- a/src/main/java/ru/r2cloud/model/Tle.java
+++ b/src/main/java/ru/r2cloud/model/Tle.java
@@ -8,6 +8,7 @@ public class Tle {
 
 	private final String[] raw;
 	private long lastUpdateTime;
+	private String source;
 
 	public Tle(String[] tle) {
 		this.raw = tle;
@@ -16,15 +17,23 @@ public Tle(String[] tle) {
 	public String[] getRaw() {
 		return raw;
 	}
-	
+
 	public void setLastUpdateTime(long lastUpdateTime) {
 		this.lastUpdateTime = lastUpdateTime;
 	}
-	
+
 	public long getLastUpdateTime() {
 		return lastUpdateTime;
 	}
 
+	public String getSource() {
+		return source;
+	}
+
+	public void setSource(String source) {
+		this.source = source;
+	}
+
 	@Override
 	public int hashCode() {
 		final int prime = 31;
@@ -56,6 +65,10 @@ public JsonObject toJson() {
 		if (raw.length > 2) {
 			json.add("line3", raw[2]);
 		}
+		json.add("updated", lastUpdateTime);
+		if (source != null) {
+			json.add("source", source);
+		}
 		return json;
 	}
 
@@ -64,7 +77,10 @@ public static Tle fromJson(JsonObject json) {
 		raw[0] = json.getString("line1", null);
 		raw[1] = json.getString("line2", null);
 		raw[2] = json.getString("line3", null);
-		return new Tle(raw);
+		Tle result = new Tle(raw);
+		result.setLastUpdateTime(json.getLong("updated", 0));
+		result.setSource(json.getString("source", null));
+		return result;
 	}
 
 }
diff --git a/src/main/java/ru/r2cloud/tle/CelestrakClient.java b/src/main/java/ru/r2cloud/tle/CelestrakClient.java
index 8a4e9092..9d4c5e8a 100644
--- a/src/main/java/ru/r2cloud/tle/CelestrakClient.java
+++ b/src/main/java/ru/r2cloud/tle/CelestrakClient.java
@@ -65,7 +65,10 @@ private Map<String, Tle> downloadTle(String location) {
 							break;
 						}
 						String noradId = line2.substring(2, 2 + 5).trim();
-						result.put(noradId, new Tle(new String[] { curLine.trim(), line1, line2 }));
+						Tle value = new Tle(new String[] { curLine.trim(), line1, line2 });
+						value.setLastUpdateTime(System.currentTimeMillis());
+						value.setSource(obj.getHost());
+						result.put(noradId, value);
 					}
 				}
 			}
diff --git a/src/main/java/ru/r2cloud/tle/TleDao.java b/src/main/java/ru/r2cloud/tle/TleDao.java
index 522d4e54..d603598c 100644
--- a/src/main/java/ru/r2cloud/tle/TleDao.java
+++ b/src/main/java/ru/r2cloud/tle/TleDao.java
@@ -13,6 +13,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+
 import ru.r2cloud.model.Tle;
 import ru.r2cloud.util.Configuration;
 import ru.r2cloud.util.Util;
@@ -63,7 +66,7 @@ private void index(Map<String, Tle> tleById) {
 		cacheByName.clear();
 		putAll(tleById);
 	}
-	
+
 	public void putAll(Map<String, Tle> tleById) {
 		cache.putAll(tleById);
 		for (Tle cur : tleById.values()) {
@@ -82,17 +85,14 @@ public long getLastUpdateTime() {
 	}
 
 	private static void saveTle(Path file, Map<String, Tle> tle) {
+		JsonArray output = new JsonArray();
+		for (Tle cur : tle.values()) {
+			output.add(cur.toJson());
+		}
 		// ensure temp and output are on the same filestore
-		Path tempOutput = file.getParent().resolve("tle.txt.tmp");
+		Path tempOutput = file.getParent().resolve("tle.json.tmp");
 		try (BufferedWriter w = Files.newBufferedWriter(tempOutput)) {
-			for (Tle cur : tle.values()) {
-				w.append(cur.getRaw()[0]);
-				w.newLine();
-				w.append(cur.getRaw()[1]);
-				w.newLine();
-				w.append(cur.getRaw()[2]);
-				w.newLine();
-			}
+			output.writeTo(w);
 		} catch (IOException e) {
 			Util.logIOException(LOG, "unable to save tle: " + file.toAbsolutePath(), e);
 			return;
@@ -110,23 +110,20 @@ private static Map<String, Tle> loadTle(Path file) {
 		if (!Files.exists(file)) {
 			return result;
 		}
+		JsonArray output = null;
 		try (BufferedReader in = Files.newBufferedReader(file)) {
-			// only first line matters
-			String curLine = null;
-			while ((curLine = in.readLine()) != null) {
-				String line1 = in.readLine();
-				if (line1 == null) {
-					break;
-				}
-				String line2 = in.readLine();
-				if (line2 == null) {
-					break;
-				}
-				String noradId = line2.substring(2, 2 + 5).trim();
-				result.put(noradId, new Tle(new String[] { curLine.trim(), line1, line2 }));
-			}
+			output = Json.parse(in).asArray();
 		} catch (IOException e) {
 			Util.logIOException(LOG, "unable to load tle from cache: " + file.toAbsolutePath(), e);
+			return result;
+		}
+		for (int i = 0; i < output.size(); i++) {
+			Tle cur = Tle.fromJson(output.get(i).asObject());
+			if (cur == null) {
+				continue;
+			}
+			String noradId = cur.getRaw()[cur.getRaw().length - 1].substring(2, 2 + 5).trim();
+			result.put(noradId, cur);
 		}
 		return result;
 	}
diff --git a/src/main/resources/config-common.properties b/src/main/resources/config-common.properties
index e068d3cb..374ab20b 100644
--- a/src/main/resources/config-common.properties
+++ b/src/main/resources/config-common.properties
@@ -8,7 +8,7 @@ ddns.interval.seconds=300
 #1 - Monday, 7 - Sunday
 tle.urls=http://r4uab.ru/satonline.txt,https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=tle,https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=tle
 tle.timeout=60000
-tle.cacheFileLocation=./data/tle.txt
+tle.cacheFileLocation=./data/tle.json
 
 housekeeping.periodMillis=3600000
 housekeeping.tle.periodMillis=172800000
diff --git a/src/test/java/ru/r2cloud/tle/HousekeepingTest.java b/src/test/java/ru/r2cloud/tle/HousekeepingTest.java
index c384915b..b98af4fc 100644
--- a/src/test/java/ru/r2cloud/tle/HousekeepingTest.java
+++ b/src/test/java/ru/r2cloud/tle/HousekeepingTest.java
@@ -130,7 +130,7 @@ public void start() throws Exception {
 		}
 
 		config = new TestConfiguration(tempFolder, FileSystems.getDefault());
-		config.setProperty("tle.cacheFileLocation", new File(tempFolder.getRoot(), "tle.txt").getAbsolutePath());
+		config.setProperty("tle.cacheFileLocation", new File(tempFolder.getRoot(), "tle.json").getAbsolutePath());
 		config.setProperty("satellites.leosatdata.location", new File(tempFolder.getRoot(), "leosatdata.json").getAbsolutePath());
 		config.setProperty("satellites.leosatdata.new.location", new File(tempFolder.getRoot(), "leosatdata.new.json").getAbsolutePath());
 		config.setProperty("satellites.satnogs.location",  new File(tempFolder.getRoot(), "satnogs.json").getAbsolutePath());
diff --git a/src/test/java/ru/r2cloud/tle/TleDaoTest.java b/src/test/java/ru/r2cloud/tle/TleDaoTest.java
index b318430a..bbb7b2eb 100644
--- a/src/test/java/ru/r2cloud/tle/TleDaoTest.java
+++ b/src/test/java/ru/r2cloud/tle/TleDaoTest.java
@@ -69,7 +69,7 @@ public void testSaveLoadAndFail() {
 	public void start() throws Exception {
 		fs = new MockFileSystem(FileSystems.getDefault());
 		config = new TestConfiguration(tempFolder, fs);
-		fileLocation = new File(tempFolder.getRoot(), "tle.txt").getAbsolutePath();
+		fileLocation = new File(tempFolder.getRoot(), "tle.json").getAbsolutePath();
 		config.setProperty("tle.cacheFileLocation", fileLocation);
 		dao = new TleDao(config);
 	}