Avatars take a long time to load after moving to S3-compatible R2

Hello,

I just migrated to R2 and everything went perfectly. All images have the S3 CDN URL link. However, I noticed an issue: avatars are taking a long time to load. On average, it takes about 3 to 4 seconds, whether I’m clicking on a user’s avatar or looking inside a post. Is this expected?

hmmm, i suspect it could be one of 3 different issues, but the most likely to me is on-the-fly resizing.

1. on-the-fly avatar resizing

when you migrated your uploads to R2, it moved the original images; however, discourse uses many different sizes of avatars (eg 45px for posts, 120px for the user card).

if those specific optimized sizes didn’t migrate perfectly, or haven’t been generated yet, dscourse has to generate them synchronously at the moment the user clicks them:

  1. discourse downloads the original avatar from R2 to the local server
  2. resizes it using imagemagick
  3. uploads the new size back to R2
  4. redirects the browser to the new URL, and this process takes 3 to 4 seconds

to verify: hard-refresh the page - if the avatar takes 3-4 seconds the first time, but loads instantly the second time, this is exactly what is happening.

to fix: this will naturally fix itself as users browse and the sizes are generated. but you can fix it instantly, by forcing the server to pre-generate all avatars in the background by ssh’ing into your server and running:

./launcher enter app
rake avatars:refresh

2. the 3 second IPv6 timeout

if the avatars are taking 3-4 seconds every time even after multiple refreshes, they are likely hitting a networking timeout.

cloudflare R2 api endpoints are dual-stack in that they use both ipv4 and ipv6. if your server droplet has an ipv6 address assigned to it, but the host’s ipv6 gateway isn’t routed properly, ruby’s internal connection to the R2 bucket will attempt ipv6 first, hang for 3 second (this is the default linux TCP timeout), fail and then instantly succeed using ipv4.

to verify: ssh into the server and run:

curl -I -6 https://cloudflare.com

if it hang for a few seconds and fails, the server’s ipv6 is broken, causing every internal S3 api check to suffer a 3-second delay.

to fix: you will need to either fix the ipv6 routing in in your host control panel or maybe even disable ipv6 on the droplet entirely

3. gravatar delays

if your site is configured to check for gravatar updates it may be pinging gravatar’s external servers before rendering the avatar. if the server has a slow outbound connection (also often related to DNS or ipv6) it will likely block the avatar render.

to verify: run this on your server
curl -I -6 https://gravatar.com
if it hangs for 3 seconds the ipv6 is broken (see above)

gravatar-related fix: in your discourse settings go to automatically download gravatars, turn it off temporarily, and see if that fixes it - i don’t think this is the problem but if it is, i you can leave the setting off, or fix the ipv6 routing as in 2 above, or maybe change the DNS resolver.

Thank you for your quick response. I think I had already tried the ‘rake avatars:refresh’ before, but I’m not absolutely sure.

What used to work for me to see the avatar open immediately was clicking on it a first time; on the second time, it would open instantly. But that’s probably due to caching. Also, I just tested your second tip and it returns a “HTTP/2 301”, with several other lines’. Same thing for tip 3. I’ll run avatars:refresh again in a few days, as I needed to restore a snapshot. Thanks again!

Gravatar

server: nginx
date: Mon, 22 Jun 2026 19:29:00 GMT
content-type: text/html; charset=utf-8
content-length: 0
content-language: en
expires: Wed, 11 Jan 1984 05:00:00 GMT
cache-control: no-cache, must-revalidate, max-age=0
x-redirect-by: Gravatar
location: https://en.gravatar.com/
alt-svc: h3=":443"; ma=86400
strict-transport-security: max-age=31536000; includeSubdomains; preload

CF

HTTP/2 301
date: Mon, 22 Jun 2026 19:27:00 GMT
content-type: text/html
content-length: 167
location: https://www.cloudflare.com/
cache-control: max-age=3600
expires: Mon, 22 Jun 2026 20:26:59 GMT
set-cookie: __cf_bm=eBP2aJ7Eg30nHPuvMMNxxKrgNtcNwKs0WDgnYyONeus-1782156420-1.0.1.1-sXpW27iuhGDF615cOfwNFybH4IMxgvZy3uA_3X_o..402T_3KSgT7CSymipL5RjdpGe3raWEqsVxQFFLPKRoDjfoT7B.0rqyDt.osbkOF98; path=/; expires=Mon, 22-Jun-26 19:57:00 GMT; domain=.cloudflare.com; HttpOnly; Secure; SameSite=None
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=QfYqSekEDPJHC2k%2BMjHN0cGjz172tmUWe2GSR8EgwNLh3TGjFYkQ0vwPxlzY1NcBcKFOMaAi4FlgjqjhETOOtHf%2BH9KdQSvqN3OME2Uh1i4nHIw%2Fy1qkvSpf4jxDchM7CaDW80tJkjBV4OqF"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
strict-transport-security: max-age=15780000; includeSubDomains
server: cloudflare
cf-ray: a0fda5d8ecd6b26d-LAX
alt-svc: h3=":443"; ma=86400

yea given your reply, i am almost certain it is issue #1 because the curl command results for cloudflare and gravatar appear to be as expected. give rake avatars:refresh a try whenever it is convenient for you and let me know if it works.

Hey Lilly, i’m still geting the same issue. Even after the rake avatars:refresh It’s even has the same issue with /latest. I tested it cleaning the cache from my browser and from cloudflare, but no success yet. Maybe i need to wait for some time? i’m testing it in a forum with 4500 users

don’t clear your browser or cloudflare caches anymore - when you run rake avatars:refresh for that many users, it doesn’t happen instantly. but rather it puts thousands of jobs into sidekiq to process in the background, which can take several hours depending on your server’s cpu. sorry about that, i should have mentioned sidekiq and that it an take awhile depending on how many users.

go to your-forum.com/sidekiq/queues and watch the queue. wait for it to completely empty out. once Sidekiq is finished, all the sizes should be in your R2 bucket permanently, and i think your avatar loading should return to normal speed.

Ok, I think something else is going on. I have nothing in my queues. But if I click on any user’s avatar, this shows up in ‘tail -f log/production.log’: Sent file /var/www/discourse/tmp/avatar_proxy/3689d91eb5e1013beef831c585b5e62edeeecbd6.jpeg (0.2ms)

oh wow ok. this is likely a smoking gun and points to a different issue.

avatar_proxy in the logs usually means discourse is refusing to serve the avatar directly from cloudflare R2 CDN. instead, discourse is aggressively intercepting the request and downloading the image from R2 to the local server’s /tmp folder, and then using ruby to serve the image to the browser. so i think this is completely bypassing the CDN and explains the 3-second delay - i suspect the server is manually fetching and loading the file on every single request :grimacing:

discourse uses the avatar_proxy in a few very specific scenarios and usually it is a privacy or security setting that forces the server to mask the external URL.

check these settings in admin - site settings:

find external system avatars url - if there is anything in that box (like /letter_avatar_proxy/v4/...), delete it so it is empty. that should stop discourse from proxying default letter avatars. also worth checking uploaded avatars allowed groups and make sure it says TL_0.

maybe doublecheck DISCOURSE_S3_CDN_URL to make sure it is correct without a trailing slash or typo?

remap custom avatars:
it seems likely that your database still contains the raw R2 bucket URLs instead of your new CDN URL; because they don’t match, your forum is likely proxying them for security reasons.

check in the rails console to see exactly what discourse is fighting with:

./launcher enter app
rails c

pick a username with a slow loading avatar

u = User.find_by_username("the_selected_username")
u.user_avatar.custom_upload.url

if the output returns a raw bucket URL your previous remaps didn’t catch everything (perhaps it might have missed a subdomain or a scheme).

to fix, ssh into your server, go back to your container again (not rails) (./launcher enter app), and run the remap tool (again lol) to swap the raw URL for your CDN URL:

discourse remap "https://<your-raw-cloudflare-url>.r2.cloudflarestorage.com" "https://cdn.your-domain.com"

then run it a second time using // instead of https:// just in case.

btw, just out of curiousity what host service are you using? i have the same general setup as you and i haven’t experienced this issue yet. so i’m also interested in your configuration and i want to try to reproduce it somehow.

the url i get shows the s3 cdn URL, and i can open the image in the browser.

I will pass the S3 for now, as i dont really need it at this time.

and i am using Advinservers for a longtime now

thank you for your help, much appreciated