diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6
index 8f2528cdd38..4a5de6d738b 100644
--- a/app/assets/javascripts/discourse/components/composer-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6
@@ -13,6 +13,9 @@ import { tinyAvatar,
          displayErrorForUpload,
          getUploadMarkdown,
          validateUploadedFiles } from 'discourse/lib/utilities';
+import { lookupCachedUploadUrl,
+         lookupUncachedUploadUrls,
+         cacheShortUploadUrl } from 'pretty-text/image-short-url';
 
 export default Ember.Component.extend({
   classNames: ['wmd-controls'],
@@ -191,6 +194,24 @@ export default Ember.Component.extend({
     $oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id));
   },
 
+  _loadShortUrls($images) {
+    const urls = _.map($images, img => $(img).data('orig-src'));
+    lookupUncachedUploadUrls(urls, ajax).then(() => this._loadCachedShortUrls($images));
+  },
+
+  _loadCachedShortUrls($images) {
+    $images.each((idx, image) => {
+      let $image = $(image);
+      let url = lookupCachedUploadUrl($image.data('orig-src'));
+      if (url) {
+        $image.removeAttr('data-orig-src');
+        if (url !== "missing") {
+          $image.attr('src', url);
+        }
+      }
+    });
+  },
+
   _warnMentionedGroups($preview) {
     Ember.run.scheduleOnce('afterRender', () => {
       var found = this.get('warnedGroupMentions') || [];
@@ -312,6 +333,7 @@ export default Ember.Component.extend({
       if (upload && upload.url) {
         if (!this._xhr || !this._xhr._userCancelled) {
           const markdown = getUploadMarkdown(upload);
+          cacheShortUploadUrl(upload.short_url, upload.url);
           this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown);
           this._resetUpload(false);
         } else {
@@ -579,6 +601,19 @@ export default Ember.Component.extend({
         Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450);
       }
 
+      // Short upload urls
+      let $shortUploadUrls = $('img[data-orig-src]');
+
+      if ($shortUploadUrls.length > 0) {
+        this._loadCachedShortUrls($shortUploadUrls);
+
+        $shortUploadUrls = $('img[data-orig-src]');
+        if ($shortUploadUrls.length > 0) {
+          // this is carefully batched so we can do an leading debounce (trigger right away)
+          Ember.run.debounce(this, this._loadShortUrls, $shortUploadUrls, 450, true);
+        }
+      }
+
       let inline = {};
       $('a.inline-onebox-loading', $preview).each(function(index, link) {
         let $link = $(link);
diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6
index 3a139c99223..a1e55234e27 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js.es6
+++ b/app/assets/javascripts/discourse/lib/utilities.js.es6
@@ -298,7 +298,7 @@ export function getUploadMarkdown(upload) {
   if (isAnImage(upload.original_filename)) {
     const split = upload.original_filename.split('.');
     const name = split[split.length-2];
-    return `![${name}|${upload.width}x${upload.height}](${upload.url})`;
+    return `![${name}|${upload.width}x${upload.height}](${upload.short_url || upload.url})`;
   } else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i).test(upload.original_filename)) {
     return uploadLocation(upload.url);
   } else {
diff --git a/app/assets/javascripts/pretty-text-bundle.js b/app/assets/javascripts/pretty-text-bundle.js
index a572344df81..41324ba31e5 100644
--- a/app/assets/javascripts/pretty-text-bundle.js
+++ b/app/assets/javascripts/pretty-text-bundle.js
@@ -10,3 +10,4 @@
 //= require ./pretty-text/sanitizer
 //= require ./pretty-text/oneboxer
 //= require ./pretty-text/inline-oneboxer
+//= require ./pretty-text/image-short-url
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6
index ee86ccc24bf..2211413f445 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6
@@ -35,7 +35,8 @@ function rule(state) {
 
   if (images.length > 0) {
     let srcList = images.map(([token, srcIndex]) => token.attrs[srcIndex][1]);
-    let longUrls = state.md.options.discourse.lookupImageUrls(srcList);
+    let lookup = state.md.options.discourse.lookupImageUrls;
+    let longUrls = (lookup && lookup(srcList)) || {};
 
     images.forEach(([token, srcIndex]) => {
       let origSrc = token.attrs[srcIndex][1];
diff --git a/app/assets/javascripts/pretty-text/image-short-url.js.es6 b/app/assets/javascripts/pretty-text/image-short-url.js.es6
new file mode 100644
index 00000000000..d815b46696c
--- /dev/null
+++ b/app/assets/javascripts/pretty-text/image-short-url.js.es6
@@ -0,0 +1,18 @@
+let _cache = {};
+
+export function lookupCachedUploadUrl(shortUrl) {
+  return _cache[shortUrl];
+}
+
+export function lookupUncachedUploadUrls(urls, ajax) {
+  return ajax('/uploads/lookup-urls', { method: 'POST', data: { short_urls: urls } })
+    .then(uploads => {
+      uploads.forEach(upload => _cache[upload.short_url] = upload.url);
+      urls.forEach(url => _cache[url] = _cache[url] || "missing");
+      return uploads;
+    });
+}
+
+export function cacheShortUploadUrl(shortUrl, url) {
+  _cache[shortUrl] = url;
+}
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index c19fa695342..da6fd016306 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -20,19 +20,32 @@ class UploadsController < ApplicationController
 
     if params[:synchronous] && (current_user.staff? || is_api?)
       data = create_upload(file, url, type, for_private_message, pasted)
-      render json: data.as_json
+      render json: serialize_upload(data)
     else
       Scheduler::Defer.later("Create Upload") do
         begin
           data = create_upload(file, url, type, for_private_message, pasted)
         ensure
-          MessageBus.publish("/uploads/#{type}", (data || {}).as_json, client_ids: [params[:client_id]])
+          MessageBus.publish("/uploads/#{type}", serialize_upload(data), client_ids: [params[:client_id]])
         end
       end
       render json: success_json
     end
   end
 
+  def lookup_urls
+    params.permit(short_urls: [])
+    uploads = []
+
+    if (params[:short_urls] && params[:short_urls].length > 0)
+      PrettyText::Helpers.lookup_image_urls(params[:short_urls]).each do |short_url, url|
+        uploads << { short_url: short_url, url: url }
+      end
+    end
+
+    render json: uploads.to_json
+  end
+
   def show
     return render_404 if !RailsMultisite::ConnectionManagement.has_db?(params[:site])
 
@@ -57,6 +70,13 @@ class UploadsController < ApplicationController
 
   protected
 
+  def serialize_upload(data)
+    # as_json.as_json is not a typo... as_json in AM serializer returns keys as symbols, we need them
+    # as strings here
+    serialized = UploadSerializer.new(data, root: nil).as_json.as_json if Upload === data
+    serialized ||= (data || {}).as_json
+  end
+
   def render_404
     raise Discourse::NotFound
   end
diff --git a/app/serializers/upload_serializer.rb b/app/serializers/upload_serializer.rb
index 9e0d866248d..a02fbef9b09 100644
--- a/app/serializers/upload_serializer.rb
+++ b/app/serializers/upload_serializer.rb
@@ -1,5 +1,11 @@
 class UploadSerializer < ApplicationSerializer
-
-  attributes :id, :url, :original_filename, :filesize, :width, :height
-
+  attributes :id,
+             :url,
+             :original_filename,
+             :filesize,
+             :width,
+             :height,
+             :extension,
+             :short_url,
+             :retain_hours
 end
diff --git a/config/routes.rb b/config/routes.rb
index 553bbc9e5b2..64fe59a0cbd 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -414,6 +414,7 @@ Discourse::Application.routes.draw do
   get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ }
 
   post "uploads" => "uploads#create"
+  post "uploads/lookup-urls" => "uploads#lookup_urls"
 
   # used to download original images
   get "uploads/:site/:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, sha: /\h{40}/, extension: /[a-z0-9\.]+/i }
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 03e0716bf26..31bf3577292 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -36,6 +36,13 @@ describe UploadsController do
         xhr :post, :create, file: logo, type: "super \# long \//\\ type with \\. $%^&*( chars" * 5
       end
 
+      it 'can look up long urls' do
+        upload = Fabricate(:upload)
+        xhr :post, :lookup_urls, short_urls: [upload.short_url]
+        result = JSON.parse(response.body)
+        expect(result[0]["url"]).to eq(upload.url)
+      end
+
       it 'is successful with an image' do
         Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything)
 
@@ -78,6 +85,7 @@ describe UploadsController do
 
         expect(response.status).to eq 200
         expect(json["id"]).to be
+        expect(json["short_url"]).to eq("upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png")
       end
 
       it 'correctly sets retain_hours for admins' do